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