UBERF-9732: Use huly id as primary social id (#8499)

* uberf-9732: use huly id as primary social id
Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com>

* uberf-9732: fix person id filter
Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com>
This commit is contained in:
Alexey Zinoviev
2025-04-09 09:52:06 +07:00
committed by GitHub
parent 420b31d66f
commit 4cb1f3e411
29 changed files with 478 additions and 238 deletions
+3 -2
View File
@@ -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",
+4 -8
View File
@@ -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
+6 -5
View File
@@ -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<string>
leaveWorkspace: (account: string) => Promise<LoginInfo | null>
leaveWorkspace: (account: AccountUuid) => Promise<LoginInfo | null>
changeUsername: (first: string, last: string) => Promise<void>
changePassword: (oldPassword: string, newPassword: string) => Promise<void>
signUpJoin: (
@@ -152,7 +153,7 @@ export interface AccountClient {
deleteIntegrationSecret: (integrationSecretKey: IntegrationSecretKey) => Promise<void>
getIntegrationSecret: (integrationSecretKey: IntegrationSecretKey) => Promise<IntegrationSecret | null>
listIntegrationsSecrets: (filter: Partial<IntegrationSecretKey>) => Promise<IntegrationSecret[]>
getAccountInfo: (uuid: PersonUuid) => Promise<AccountInfo>
getAccountInfo: (uuid: AccountUuid) => Promise<AccountInfo>
setCookie: () => Promise<void>
deleteCookie: () => Promise<void>
@@ -355,7 +356,7 @@ class AccountClientImpl implements AccountClient {
return await this.rpc(request)
}
async leaveWorkspace (account: string): Promise<LoginInfo | null> {
async leaveWorkspace (account: AccountUuid): Promise<LoginInfo | null> {
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<AccountInfo> {
async getAccountInfo (uuid: AccountUuid): Promise<AccountInfo> {
const request = {
method: 'getAccountInfo' as const,
params: { accountId: uuid }
+2 -2
View File
@@ -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
}
+2 -1
View File
@@ -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<T> (id: T | undefined | null): id is T {
@@ -0,0 +1,202 @@
<!--
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import contact, { Person, SocialIdentity } from '@hcengineering/contact'
import core, { FindResult, getObjectValue, includesAny, PersonId, Ref, Space, WithLookup } from '@hcengineering/core'
import presentation, { getClient } from '@hcengineering/presentation'
import ui, {
deviceOptionsStore,
EditWithIcon,
Icon,
IconCheck,
IconSearch,
Loading,
resizeObserver
} from '@hcengineering/ui'
import view, { Filter } from '@hcengineering/view'
import { FILTER_DEBOUNCE_MS, sortFilterValues } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import PersonPresenter from './PersonPresenter.svelte'
export let filter: Filter
export let space: Ref<Space> | undefined = undefined
export let onChange: (e: Filter) => void
const client = getClient()
filter.modes = filter.modes === undefined ? [view.filter.FilterObjectIn, view.filter.FilterObjectNin] : filter.modes
filter.mode = filter.mode === undefined ? filter.modes[0] : filter.mode
let socialIdentities: (WithLookup<SocialIdentity> | undefined | null)[] = []
let socialIdentitiesPromise: Promise<FindResult<SocialIdentity>> | undefined
let personIdToPersonMap: Record<PersonId, Ref<Person>> = {}
let personToPersonIdsMap: Record<Ref<Person>, PersonId[]> = {}
let persons: Ref<Person>[] = []
const targets = new Set<any>()
let filterUpdateTimeout: any | undefined
let search: string = ''
const dispatch = createEventDispatcher()
async function getValues (search: string): Promise<void> {
if (socialIdentitiesPromise !== undefined) {
await socialIdentitiesPromise
}
targets.clear()
const spaces = (
await client.findAll(core.class.Space, { archived: true }, { projection: { _id: 1, archived: 1, _class: 1 } })
).map((it) => it._id)
const baseObjects = await client.findAll(
filter.key._class,
space !== undefined ? { space } : { space: { $nin: spaces } },
{
projection: { [filter.key.key]: 1, space: 1 }
}
)
for (const object of baseObjects) {
const socialIdentity = getObjectValue(filter.key.key, object) ?? undefined
targets.add(socialIdentity)
}
for (const object of filter.value) {
targets.add(object)
}
const resultQuery =
search !== ''
? {
'$lookup.attachedTo.name': { $like: '%' + search + '%' },
_id: { $in: Array.from(targets.keys()) }
}
: {
_id: { $in: Array.from(targets.keys()) }
}
socialIdentitiesPromise = client.findAll(contact.class.SocialIdentity, resultQuery, {
lookup: { attachedTo: contact.class.Person }
})
socialIdentities = await socialIdentitiesPromise
if (targets.has(undefined)) {
socialIdentities.unshift(undefined)
}
personIdToPersonMap = (socialIdentities ?? []).reduce<Record<string, Ref<Person>>>((acc, sid) => {
if (sid == null) return acc
const person = sid?.$lookup?.attachedTo
if (person == null) return acc
acc[sid._id] = person._id
return acc
}, {})
personToPersonIdsMap = (socialIdentities ?? []).reduce<Record<Ref<Person>, PersonId[]>>((acc, sid) => {
if (sid == null) return acc
const person = sid?.$lookup?.attachedTo
if (person == null) return acc
if (acc[person._id] == null) {
acc[person._id] = []
}
acc[person._id].push(sid._id)
return acc
}, {})
persons = sortFilterValues(Array.from(new Set(Object.values(personIdToPersonMap))), (p) =>
isPersonSelected(p, filter.value)
)
socialIdentitiesPromise = undefined
}
function isPersonSelected (person: Ref<Person>, selectedIds: any[]): boolean {
const personSocialIds = personToPersonIdsMap[person] ?? []
return includesAny(personSocialIds, selectedIds)
}
function handleFilterToggle (person: Ref<Person>): void {
const personSocialIds = personToPersonIdsMap[person] ?? []
if (isPersonSelected(person, filter.value)) {
filter.value = filter.value.filter((p) => !personSocialIds.includes(p))
} else {
filter.value = [...filter.value, ...personSocialIds]
}
updateFilter()
}
function updateFilter (): void {
clearTimeout(filterUpdateTimeout)
filterUpdateTimeout = setTimeout(() => {
onChange(filter)
}, FILTER_DEBOUNCE_MS)
}
$: {
void getValues(search)
}
</script>
<div class="selectPopup" use:resizeObserver={() => dispatch('changeContent')}>
<div class="header">
<EditWithIcon
icon={IconSearch}
size={'large'}
width={'100%'}
autoFocus={!$deviceOptionsStore.isMobile}
bind:value={search}
placeholder={presentation.string.Search}
/>
</div>
<div class="scroll">
<div class="box">
{#if socialIdentitiesPromise}
<Loading />
{:else}
{#each persons as person}
<button
class="menu-item no-focus flex-row-center"
on:click={() => {
handleFilterToggle(person)
}}
>
<div class="clear-mins flex-grow">
<PersonPresenter
value={person}
shouldShowPlaceholder
defaultName={ui.string.NotSelected}
disabled
noUnderline
/>
</div>
<div class="check pointer-events-none">
{#if isPersonSelected(person, filter.value)}
<Icon icon={IconCheck} size={'small'} />
{/if}
</div>
</button>
{/each}
{/if}
</div>
</div>
<div class="menu-space" />
</div>
+7 -3
View File
@@ -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<Contact>): ObjectSearchResult => ({
@@ -299,7 +302,7 @@ async function kickEmployee (doc: Person): Promise<void> {
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<Resources> => ({
EditOrganizationPanel,
ChannelIcon,
SpaceMembersEditor,
ContactNamePresenter
ContactNamePresenter,
PersonIdFilter
},
completion: {
EmployeeQuery: async (
+2 -1
View File
@@ -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<ChannelProvider>,
+3 -17
View File
@@ -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<Person>): Promise<PersonId | undefined> {
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<PersonId[]> {
@@ -393,12 +385,6 @@ export async function getAllAccounts (client: Client): Promise<AccountUuid[]> {
return employees.map((it) => it.personUuid).filter(notEmpty)
}
export async function getAllEmployeesPrimarySocialStrings (client: Client): Promise<PersonId[]> {
const socialStringsByPerson = getSocialStringsByEmployee(client)
return Object.values(socialStringsByPerson).map((it) => pickPrimarySocialId(it))
}
export async function getAllUserAccounts (client: Client): Promise<AccountUuid[]> {
const employees = await client.findAll(contact.mixin.Employee, { active: true })
+2 -1
View File
@@ -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<void
}
}
export async function leaveWorkspace (account: string): Promise<LoginInfo | null> {
export async function leaveWorkspace (account: AccountUuid): Promise<LoginInfo | null> {
return await getAccountClient().leaveWorkspace(account)
}
@@ -0,0 +1,26 @@
<!--
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { Ref, Space } from '@hcengineering/core'
import { Component } from '@hcengineering/ui'
import contact from '@hcengineering/contact'
import { Filter } from '@hcengineering/view'
export let filter: Filter
export let space: Ref<Space> | undefined = undefined
export let onChange: (e: Filter) => void
</script>
<Component is={contact.component.PersonIdFilter} props={{ filter, space, onChange }} />
@@ -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<Person>[] = []
$: void getPersons(value.map((p) => p[1]).flat())
$: void getPersons(value)
async function getPersons (ids: PersonId[]): Promise<void> {
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)
}
}
}
</script>
+3
View File
@@ -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<Resources> => ({
StatusRefPresenter,
PersonArrayEditor,
PersonIdPresenter,
PersonIdFilter,
PersonIdFilterValuePresenter,
DateFilterPresenter,
StringFilterPresenter,
+1
View File
@@ -175,6 +175,7 @@ const view = plugin(viewId, {
SearchSelector: '' as AnyComponent,
FoldersBrowser: '' as AnyComponent,
PersonIdPresenter: '' as AnyComponent,
PersonIdFilter: '' as AnyComponent,
RolePresenter: '' as AnyComponent
},
ids: {
+15 -11
View File
@@ -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<Event>
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<Event>, 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<Person>)
if (socialStrings.length === 0) continue
const socialIds = await getSocialIds(control, part as Ref<Person>)
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(
+9 -25
View File
@@ -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<Person | undefined> {
const { type, value } = parseSocialIdString(control.txFactory.account)
@@ -39,13 +33,16 @@ export async function getCurrentPerson (control: TriggerControl): Promise<Person
)[0]
}
export async function getSocialStrings (control: TriggerControl, person: Ref<Person>): Promise<PersonId[]> {
const socialIdentities = await control.findAll(control.ctx, contact.class.SocialIdentity, {
export async function getSocialIds (control: TriggerControl, person: Ref<Person>): Promise<SocialId[]> {
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<Person>): Promise<PersonId[]> {
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<Record<AccountUuid, PersonId>> {
return Object.entries(await getSocialIdsByAccounts(control, accounts)).reduce<Record<AccountUuid, PersonId>>(
(acc, [account, sids]) => {
acc[account as AccountUuid] = pickPrimarySocialId(sids)
return acc
},
{}
)
}
export async function getAccountBySocialId (control: TriggerControl, socialId: PersonId): Promise<AccountUuid | null> {
const contextAccount = control.ctx.contextData.socialStringsToUsers.get(socialId)
if (contextAccount != null) {
@@ -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<Ref<Employee>> = documentTraining.trainees
const roles = documentTraining.roles
@@ -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 <ObjectId, UUID>
const accountsIdToUuid: Record<string, PersonUuid> = {}
const accountsIdToUuid: Record<string, AccountUuid> = {}
// Mapping between <email, UUID>
const accountsEmailToUuid: Record<string, PersonUuid> = {}
const accountsEmailToUuid: Record<string, AccountUuid> = {}
// Mapping between <OldId, UUID>
const workspacesIdToUuid: Record<WorkspaceDataId, WorkspaceUuid> = {}
@@ -176,7 +177,7 @@ export async function migrateFromOldAccounts (oldAccsUrl: string, accountDB: Acc
}
}
async function migrateAccount (account: OldAccount, accountDB: AccountDB): Promise<PersonUuid | undefined> {
async function migrateAccount (account: OldAccount, accountDB: AccountDB): Promise<AccountUuid | undefined> {
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<string, PersonUuid>,
accountsEmailToUuid: Record<string, PersonUuid>
accountsIdToUuid: Record<string, AccountUuid>,
accountsEmailToUuid: Record<string, AccountUuid>
): Promise<WorkspaceUuid | undefined> {
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
+14 -13
View File
@@ -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')
+12 -12
View File
@@ -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')
}
+7 -4
View File
@@ -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()
+10 -10
View File
@@ -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<WorkspaceS
interface WorkspaceMember {
workspaceUuid: WorkspaceUuid
accountUuid: PersonUuid
accountUuid: AccountUuid
role: AccountRole
}
@@ -530,7 +530,7 @@ export class MongoAccountDB implements AccountDB {
}
}
async assignWorkspace (accountId: PersonUuid, workspaceId: WorkspaceUuid, role: AccountRole): Promise<void> {
async assignWorkspace (accountId: AccountUuid, workspaceId: WorkspaceUuid, role: AccountRole): Promise<void> {
await this.workspaceMembers.insertOne({
workspaceUuid: workspaceId,
accountUuid: accountId,
@@ -538,7 +538,7 @@ export class MongoAccountDB implements AccountDB {
})
}
async unassignWorkspace (accountId: PersonUuid, workspaceId: WorkspaceUuid): Promise<void> {
async unassignWorkspace (accountId: AccountUuid, workspaceId: WorkspaceUuid): Promise<void> {
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<void> {
async updateWorkspaceRole (accountId: AccountUuid, workspaceId: WorkspaceUuid, role: AccountRole): Promise<void> {
await this.workspaceMembers.updateOne(
{
workspaceUuid: workspaceId,
@@ -707,7 +707,7 @@ export class MongoAccountDB implements AccountDB {
)
}
async getWorkspaceRole (accountId: PersonUuid, workspaceId: WorkspaceUuid): Promise<AccountRole | null> {
async getWorkspaceRole (accountId: AccountUuid, workspaceId: WorkspaceUuid): Promise<AccountRole | null> {
const assignment = await this.workspaceMembers.findOne({
workspaceUuid: workspaceId,
accountUuid: accountId
@@ -723,18 +723,18 @@ export class MongoAccountDB implements AccountDB {
}))
}
async getAccountWorkspaces (accountId: PersonUuid): Promise<WorkspaceInfoWithStatus[]> {
async getAccountWorkspaces (accountId: AccountUuid): Promise<WorkspaceInfoWithStatus[]> {
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<void> {
async setPassword (accountId: AccountUuid, passwordHash: Buffer, salt: Buffer): Promise<void> {
await this.account.updateOne({ uuid: accountId }, { hash: passwordHash, salt })
}
async resetPassword (accountId: PersonUuid): Promise<void> {
async resetPassword (accountId: AccountUuid): Promise<void> {
await this.account.updateOne({ uuid: accountId }, { hash: null, salt: null })
}
}
+9 -9
View File
@@ -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<void> {
async assignWorkspace (accountUuid: AccountUuid, workspaceUuid: WorkspaceUuid, role: AccountRole): Promise<void> {
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<void> {
async unassignWorkspace (accountUuid: AccountUuid, workspaceUuid: WorkspaceUuid): Promise<void> {
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<void> {
async updateWorkspaceRole (accountUuid: AccountUuid, workspaceUuid: WorkspaceUuid, role: AccountRole): Promise<void> {
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<AccountRole | null> {
async getWorkspaceRole (accountUuid: AccountUuid, workspaceUuid: WorkspaceUuid): Promise<AccountRole | null> {
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<WorkspaceInfoWithStatus[]> {
async getAccountWorkspaces (accountUuid: AccountUuid): Promise<WorkspaceInfoWithStatus[]> {
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<void> {
async setPassword (accountUuid: AccountUuid, hash: Buffer, salt: Buffer): Promise<void> {
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<void> {
async resetPassword (accountUuid: AccountUuid): Promise<void> {
await this
.client`DELETE FROM ${this.client(this.account.getPasswordsTableName())} WHERE account_uuid = ${accountUuid}`
}
+27 -51
View File
@@ -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<string, string> = 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<OtpInfo> {
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<LoginInfo> {
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<LoginInfo | null> {
const { account: targetAccount } = params
const { account, workspace } = decodeTokenVerbose(ctx, token)
@@ -1254,7 +1258,7 @@ export async function getLoginInfoByToken (
branding: Branding | null,
token: string
): Promise<LoginInfo | WorkspaceLoginInfo> {
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<PersonInfo> {
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<AccountInfo> {
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<Record<AccountMe
getLoginInfoByToken: wrap(getLoginInfoByToken),
getSocialIds: wrap(getSocialIds),
getPerson: wrap(getPerson),
getPersonInfo: wrap(getPersonInfo),
findPersonBySocialKey: wrap(findPersonBySocialKey),
findPersonBySocialId: wrap(findPersonBySocialId),
findSocialIdBySocialKey: wrap(findSocialIdBySocialKey),
+36 -8
View File
@@ -20,11 +20,13 @@ import {
SocialIdType,
Version,
WorkspaceMode,
type PersonInfo,
type BackupStatus,
type Branding,
type PersonId,
type PersonUuid,
type WorkspaceUuid
type WorkspaceUuid,
type AccountUuid
} from '@hcengineering/core'
import platform, { getMetadata, PlatformError, Severity, Status, unknownError } from '@hcengineering/platform'
import { decodeTokenVerbose } from '@hcengineering/server-token'
@@ -57,7 +59,8 @@ import {
wrap,
addSocialId,
getWorkspaces,
updateWorkspaceRole
updateWorkspaceRole,
getPersonName
} from './utils'
// Note: it is IMPORTANT to always destructure params passed here to avoid sending extra params
@@ -201,17 +204,14 @@ export async function updateWorkspaceRoleBySocialKey (
): Promise<void> {
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<PersonInfo> {
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<Record<AccountServiceMethods, Acco
performWorkspaceOperation: wrap(performWorkspaceOperation),
updateWorkspaceRoleBySocialKey: wrap(updateWorkspaceRoleBySocialKey),
addSocialIdToPerson: wrap(addSocialIdToPerson),
getPersonInfo: wrap(getPersonInfo),
createIntegration: wrap(createIntegration),
updateIntegration: wrap(updateIntegration),
deleteIntegration: wrap(deleteIntegration),
+12 -11
View File
@@ -27,7 +27,8 @@ import {
type PersonUuid,
type WorkspaceUuid,
type WorkspaceDataId,
type PersonId
type PersonId,
type AccountUuid
} from '@hcengineering/core'
/* ========= D A T A B A S E E N T I T I E S ========= */
@@ -50,7 +51,7 @@ export interface SocialId extends SocialIdBase {
}
export interface Account {
uuid: PersonUuid
uuid: AccountUuid
automatic?: boolean
timezone?: string
locale?: string
@@ -60,7 +61,7 @@ export interface Account {
// TODO: type data with generic type
export interface AccountEvent {
accountUuid: PersonUuid
accountUuid: AccountUuid
eventType: AccountEventType
data?: Record<string, any>
time: Timestamp
@@ -190,12 +191,12 @@ export interface AccountDB {
init: () => Promise<void>
createWorkspace: (data: WorkspaceData, status: WorkspaceStatusData) => Promise<WorkspaceUuid>
assignWorkspace: (accountId: PersonUuid, workspaceId: WorkspaceUuid, role: AccountRole) => Promise<void>
updateWorkspaceRole: (accountId: PersonUuid, workspaceId: WorkspaceUuid, role: AccountRole) => Promise<void>
unassignWorkspace: (accountId: PersonUuid, workspaceId: WorkspaceUuid) => Promise<void>
getWorkspaceRole: (accountId: PersonUuid, workspaceId: WorkspaceUuid) => Promise<AccountRole | null>
assignWorkspace: (accountId: AccountUuid, workspaceId: WorkspaceUuid, role: AccountRole) => Promise<void>
updateWorkspaceRole: (accountId: AccountUuid, workspaceId: WorkspaceUuid, role: AccountRole) => Promise<void>
unassignWorkspace: (accountId: AccountUuid, workspaceId: WorkspaceUuid) => Promise<void>
getWorkspaceRole: (accountId: AccountUuid, workspaceId: WorkspaceUuid) => Promise<AccountRole | null>
getWorkspaceMembers: (workspaceId: WorkspaceUuid) => Promise<WorkspaceMemberInfo[]>
getAccountWorkspaces: (accountId: PersonUuid) => Promise<WorkspaceInfoWithStatus[]>
getAccountWorkspaces: (accountId: AccountUuid) => Promise<WorkspaceInfoWithStatus[]>
getPendingWorkspace: (
region: string,
version: Data<Version>,
@@ -203,8 +204,8 @@ export interface AccountDB {
processingTimeoutMs: number,
wsLivenessMs?: number
) => Promise<WorkspaceInfoWithStatus | undefined>
setPassword: (accountId: PersonUuid, passwordHash: Buffer, salt: Buffer) => Promise<void>
resetPassword: (accountId: PersonUuid) => Promise<void>
setPassword: (accountId: AccountUuid, passwordHash: Buffer, salt: Buffer) => Promise<void>
resetPassword: (accountId: AccountUuid) => Promise<void>
}
export interface DbCollection<T> {
@@ -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
+27 -19
View File
@@ -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<void> {
if (password == null || password === '') {
@@ -444,17 +445,15 @@ export async function createAccount (
createdOn = Date.now()
): Promise<void> {
// 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<void> {
@@ -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<void> {
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<void> {
await db.invite.updateOne({ id: inviteId }, { $inc: { remainingUses: -1 } })
}
export async function getAccount (db: AccountDB, uuid: PersonUuid): Promise<Account | null> {
export async function getAccount (db: AccountDB, uuid: AccountUuid): Promise<Account | null> {
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<WorkspaceLoginInfo> {
@@ -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<AccountRole | null> {
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<void> {
+4 -4
View File
@@ -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
@@ -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