UBERF-9521: Refactor session manager (#8560)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev
2025-04-16 20:50:58 +07:00
committed by GitHub
parent 93f40761b0
commit 0c9c985ab8
30 changed files with 897 additions and 750 deletions
+10 -8
View File
@@ -97,7 +97,7 @@
"MODEL_JSON": "${workspaceRoot}/models/all/bundle/model.json",
// "SERVER_PROVIDER":"uweb"
"SERVER_PROVIDER": "ws",
"MODEL_VERSION": "0.7.48",
"MODEL_VERSION": "0.7.75",
// "VERSION": "0.6.289",
"ELASTIC_INDEX_NAME": "local_storage_index",
"UPLOAD_URL": "/files",
@@ -120,9 +120,9 @@
"args": ["src/__start.ts"],
"env": {
"FULLTEXT_URL": "http://localhost:4710",
"DB_URL": "mongodb://localhost:27018",
// "DB_URL": "mongodb://localhost:27018",
// "DB_URL": "postgresql://postgres:example@localhost:5432",
// "DB_URL": "postgresql://root@huly.local:26258/defaultdb?sslmode=disable",
"DB_URL": "postgresql://root@huly.local:26258/defaultdb?sslmode=disable",
// "GREEN_URL": "http://huly.local:6767?token=secret",
"SERVER_PORT": "3334",
"METRICS_CONSOLE": "false",
@@ -135,8 +135,9 @@
"FRONT_URL": "http://localhost:8083",
"ACCOUNTS_URL": "http://localhost:3003",
"MODEL_JSON": "${workspaceRoot}/models/all/bundle/model.json",
"MODEL_VERSION": "0.7.1",
"STATS_URL": "http://huly.local:4901"
"MODEL_VERSION": "0.7.75",
"STATS_URL": "http://huly.local:4901",
"QUEUE_CONFIG": "localhost:19093"
},
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
"runtimeVersion": "20",
@@ -213,12 +214,13 @@
"request": "launch",
"args": ["src/__start.ts"],
"env": {
"MONGO_URL": "mongodb://localhost:27018",
"DB_URL": "mongodb://localhost:27018",
// "MONGO_URL": "mongodb://localhost:27018",
// "DB_URL": "mongodb://localhost:27018",
// "DB_URL": "postgresql://postgres:example@localhost:5432",
"DB_URL": "postgresql://root@huly.local:26258/defaultdb?sslmode=disable",
"SERVER_SECRET": "secret",
"REGION_INFO": "|Mongo;pg|Postgres;cockroach|CockroachDB",
"TRANSACTOR_URL": "ws://huly.local:3334;;,ws://huly.local:3335;;europe",
"TRANSACTOR_URL": "ws://transactor:3334;ws://localhost:3334",
"ACCOUNTS_URL": "http://localhost:3003",
"ACCOUNT_PORT": "3003",
"FRONT_URL": "http://localhost:8083",
+6 -5
View File
@@ -774,7 +774,7 @@ importers:
version: file:projects/pod-print.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(utf-8-validate@6.0.4)
'@rush-temp/pod-server':
specifier: file:./projects/pod-server.tgz
version: file:projects/pod-server.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(utf-8-validate@6.0.4)
version: file:projects/pod-server.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))
'@rush-temp/pod-ses':
specifier: file:./projects/pod-ses.tgz
version: file:projects/pod-ses.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))
@@ -4806,7 +4806,7 @@ packages:
version: 0.0.0
'@rush-temp/pod-server@file:projects/pod-server.tgz':
resolution: {integrity: sha512-nlXF3FUjnhvEM9Jz2qnS0pYDDJC4Akt9EZEmku7H+TFP6znCb/pBVelUlY5W+CSRBUFpRkmNv2e/KS/knh4b7w==, tarball: file:projects/pod-server.tgz}
resolution: {integrity: sha512-9Ff7F+voylF+V6WMOoCiftEMWTcPA4odM1CleTnf6cfA8Kg8vUz4Di2kzNcH/yxmiywvp47OGSsfuly+djtnkw==, tarball: file:projects/pod-server.tgz}
version: 0.0.0
'@rush-temp/pod-ses@file:projects/pod-ses.tgz':
@@ -5274,7 +5274,7 @@ packages:
version: 0.0.0
'@rush-temp/server@file:projects/server.tgz':
resolution: {integrity: sha512-zYTPrh4adUZ36sz8IfoCcJ2yGCSbXgLaeYBv4++J+g/njPytlCfEN9L1x5aYT5zu91cZ/Gt4DkHGFgHsPwNjAw==, tarball: file:projects/server.tgz}
resolution: {integrity: sha512-ZeFqWmcBAIlHDDCSms07YvcEuy5G2LJ/Y4wAw743ykDDgqxQ7ptxJpQ0DATsSdOnAF916DE8TYktKbV9Hu1cug==, tarball: file:projects/server.tgz}
version: 0.0.0
'@rush-temp/setting-assets@file:projects/setting-assets.tgz':
@@ -22126,7 +22126,7 @@ snapshots:
- supports-color
- utf-8-validate
'@rush-temp/pod-server@file:projects/pod-server.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(utf-8-validate@6.0.4)':
'@rush-temp/pod-server@file:projects/pod-server.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))':
dependencies:
'@hcengineering/communication-server': 0.1.170(typescript@5.3.3)
'@types/body-parser': 1.19.5
@@ -22160,6 +22160,7 @@ snapshots:
ts-jest: 29.1.2(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.24.2)(jest@29.7.0(@types/node@20.11.19)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3)))(typescript@5.3.3)
ts-node: 10.9.2(@types/node@20.11.19)(typescript@5.3.3)
typescript: 5.3.3
utf-8-validate: 6.0.4
ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)
transitivePeerDependencies:
- '@babel/core'
@@ -22170,7 +22171,6 @@ snapshots:
- babel-plugin-macros
- node-notifier
- supports-color
- utf-8-validate
'@rush-temp/pod-ses@file:projects/pod-ses.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))':
dependencies:
@@ -25474,6 +25474,7 @@ snapshots:
prettier: 3.2.5
ts-jest: 29.1.2(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.24.2)(jest@29.7.0(@types/node@20.11.19)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3)))(typescript@5.3.3)
typescript: 5.3.3
utf-8-validate: 6.0.4
transitivePeerDependencies:
- '@babel/core'
- '@jest/types'
+1 -1
View File
@@ -164,7 +164,7 @@ export async function backupRestore (
recheck: false,
storageAdapter: workspaceStorage,
getConnection: async () => {
return wrapPipeline(ctx, await pipelineFactory(ctx, wsUrl, true, () => {}, null, null), wsUrl)
return wrapPipeline(ctx, await pipelineFactory(ctx, wsUrl, () => {}, null, null), wsUrl)
}
})
)
+26 -15
View File
@@ -13,38 +13,39 @@
// limitations under the License.
//
import {
type AccountRole,
type AccountInfo,
type AccountRole,
type AccountUuid,
BackupStatus,
concatLink,
Data,
type Person,
type PersonUuid,
type PersonId,
type PersonInfo,
type PersonUuid,
type SocialIdType,
Version,
type WorkspaceInfoWithStatus,
type WorkspaceMemberInfo,
WorkspaceMode,
concatLink,
type WorkspaceUserOperation,
type WorkspaceUuid,
type PersonId,
type SocialIdType,
type AccountUuid
type WorkspaceUuid
} from '@hcengineering/core'
import platform, { PlatformError, Severity, Status } from '@hcengineering/platform'
import type {
LoginInfo,
MailboxOptions,
OtpInfo,
WorkspaceLoginInfo,
RegionInfo,
WorkspaceOperation,
MailboxInfo,
Integration,
IntegrationKey,
IntegrationSecret,
IntegrationSecretKey,
SocialId
LoginInfo,
LoginInfoWithWorkspaces,
MailboxInfo,
MailboxOptions,
OtpInfo,
RegionInfo,
SocialId,
WorkspaceLoginInfo,
WorkspaceOperation
} from './types'
import { getClientTimezone } from './utils'
@@ -63,6 +64,7 @@ export interface AccountClient {
validateOtp: (email: string, code: string) => Promise<LoginInfo>
loginOtp: (email: string) => Promise<OtpInfo>
getLoginInfoByToken: () => Promise<LoginInfo | WorkspaceLoginInfo>
getLoginWithWorkspaceInfo: () => Promise<LoginInfoWithWorkspaces>
restorePassword: (password: string) => Promise<LoginInfo>
confirm: () => Promise<LoginInfo>
requestPasswordReset: (email: string) => Promise<void>
@@ -308,6 +310,15 @@ class AccountClientImpl implements AccountClient {
return await this.rpc(request)
}
async getLoginWithWorkspaceInfo (): Promise<LoginInfoWithWorkspaces> {
const request = {
method: 'getLoginWithWorkspaceInfo' as const,
params: {}
}
return await this.rpc(request)
}
async restorePassword (password: string): Promise<LoginInfo> {
const request = {
method: 'restorePassword' as const,
+29 -1
View File
@@ -6,7 +6,8 @@ import {
type AccountRole,
type Timestamp,
type SocialId as SocialIdBase,
PersonUuid
PersonUuid,
type WorkspaceMode
} from '@hcengineering/core'
export interface LoginInfo {
@@ -16,6 +17,33 @@ export interface LoginInfo {
token?: string
}
export interface EndpointInfo {
internalUrl: string
externalUrl: string
region: string
}
export interface WorkspaceVersion {
versionMajor: number
versionMinor: number
versionPatch: number
}
export interface LoginInfoWorkspace {
url: string
dataId?: WorkspaceDataId
mode: WorkspaceMode
version: WorkspaceVersion
endpoint: EndpointInfo
role: AccountRole | null
progress?: number
}
export interface LoginInfoWithWorkspaces extends LoginInfo {
// Information necessary to handle user <--> transactor connectivity.
workspaces: Record<WorkspaceUuid, LoginInfoWorkspace>
socialIds: SocialId[]
}
/**
* @public
*/
-1
View File
@@ -50,7 +50,6 @@ export interface SessionData {
admin?: boolean
isTriggerCtx?: boolean
workspace: WorkspaceIds
branding: Branding | null
socialStringsToUsers: Map<PersonId, AccountUuid>
asyncRequests?: (() => Promise<void>)[]
+1
View File
@@ -82,6 +82,7 @@
"@hcengineering/server-storage": "^0.6.0",
"@hcengineering/server-telegram": "^0.6.0",
"@hcengineering/server-token": "^0.6.11",
"utf-8-validate": "^6.0.4",
"bufferutil": "^4.0.8",
"msgpackr": "^1.11.2",
"msgpackr-extract": "^3.0.3",
+5 -30
View File
@@ -16,11 +16,7 @@ import serverCalendar from '@hcengineering/server-calendar'
import serverCore, {
initStatisticsContext,
loadBrandingMap,
type ConnectionSocket,
type Session,
type StorageConfiguration,
type UserStatistics,
type Workspace,
type WorkspaceStatistics
} from '@hcengineering/server-core'
import serverNotification from '@hcengineering/server-notification'
@@ -33,7 +29,7 @@ import { profileStart, profileStop } from './inspector'
configureAnalytics(process.env.SENTRY_DSN, {})
Analytics.setTag('application', 'transactor')
let getUsers: () => WorkspaceStatistics[] = () => {
let getStats: () => WorkspaceStatistics[] = () => {
return []
}
@@ -50,8 +46,8 @@ void queue.createTopics(10).catch((err) => {
// Force create server metrics context with proper logging
const metricsContext = initStatisticsContext('transactor', {
getUsers: (): WorkspaceStatistics[] => {
return getUsers()
getStats: (): WorkspaceStatistics[] => {
return getStats()
},
factory: () =>
new MeasureMetricsContext(
@@ -103,29 +99,8 @@ const { shutdown, sessionManager } = start(metricsContext, config.dbUrl, {
queue
})
const entryToUserStats = (session: Session, socket: ConnectionSocket): UserStatistics => {
return {
current: session.current,
mins5: session.mins5,
userId: session.getUser(),
sessionId: socket.id,
total: session.total,
data: socket.data
}
}
const workspaceToWorkspaceStats = (ws: Workspace): WorkspaceStatistics => {
return {
clientsTotal: new Set(Array.from(ws.sessions.values()).map((it) => it.session.getUser())).size,
sessionsTotal: ws.sessions.size,
workspaceName: ws.workspaceName,
wsId: ws.workspaceUuid,
sessions: Array.from(ws.sessions.values()).map((it) => entryToUserStats(it.session, it.socket))
}
}
getUsers = () => {
return Array.from(sessionManager.workspaces.values()).map((it) => workspaceToWorkspaceStats(it))
getStats = (): WorkspaceStatistics[] => {
return sessionManager.getStatistics()
}
const close = (): void => {
+1 -3
View File
@@ -40,7 +40,7 @@ import {
type TxResult,
type WorkspaceUuid
} from '@hcengineering/core'
import { ClientSession, startSessionManager, type SessionManagerOptions } from '@hcengineering/server'
import { startSessionManager, type SessionManagerOptions } from '@hcengineering/server'
import { createDummyQueue, createDummyStorageAdapter } from '@hcengineering/server-core'
import { startHttpServer } from '../server_http'
import { genMinModel } from './minmodel'
@@ -100,7 +100,6 @@ describe('server', () => {
communicationApiFactory: async () => {
return {} as any
},
sessionFactory: (token, workspace, account) => new ClientSession(token, workspace, account, true),
brandingMap: {},
accountsUrl: '',
queue: createDummyQueue()
@@ -216,7 +215,6 @@ describe('server', () => {
communicationApiFactory: async () => {
return {} as any
},
sessionFactory: (token, workspace, account) => new ClientSession(token, workspace, account, true),
brandingMap: {},
accountsUrl: '',
queue: createDummyQueue()
+3 -10
View File
@@ -14,19 +14,16 @@
// limitations under the License.
//
import { type Account, type BrandingMap, type MeasureContext, type Tx } from '@hcengineering/core'
import { type BrandingMap, type MeasureContext, type Tx } from '@hcengineering/core'
import { buildStorageFromConfig } from '@hcengineering/server-storage'
import { ClientSession, startSessionManager } from '@hcengineering/server'
import { startSessionManager } from '@hcengineering/server'
import {
type CommunicationApiFactory,
type PlatformQueue,
type Session,
type SessionManager,
type StorageConfiguration,
type Workspace
type StorageConfiguration
} from '@hcengineering/server-core'
import { type Token } from '@hcengineering/server-token'
import { Api as CommunicationApi } from '@hcengineering/communication-server'
import {
@@ -120,9 +117,6 @@ export function start (
{ ...opt, externalStorage, adapterSecurity: isAdapterSecurity(dbUrl), queue: opt.queue },
{}
)
const sessionFactory = (token: Token, workspace: Workspace, account: Account): Session => {
return new ClientSession(token, workspace, account, token.extra?.mode === 'backup')
}
const communicationApiFactory: CommunicationApiFactory = async (ctx, workspace, broadcastSessions) => {
if (dbUrl.startsWith('mongodb')) {
return {
@@ -150,7 +144,6 @@ export function start (
const sessionManager = startSessionManager(metrics, {
pipelineFactory,
sessionFactory,
communicationApiFactory,
brandingMap: opt.brandingMap,
enableCompression: opt.enableCompression,
+18 -22
View File
@@ -89,7 +89,6 @@ const rpcHandler = new RPCHandler()
const backpressureSize = 100 * 1024
/**
* @public
* @param sessionFactory -
* @param port -
* @param host -
*/
@@ -403,27 +402,24 @@ export function startHttpServer (
try {
const token = (req.query.token as string) ?? (req.headers.authorization ?? '').split(' ')[1]
decodeToken(token)
const ws = sessions.workspaces.get(req.query.workspace as WorkspaceUuid)
if (ws !== undefined) {
// push the data to body
void retrieveJson(req)
.then((data) => {
if (Array.isArray(data)) {
sessions.broadcastAll(ws, data as Tx[])
} else {
sessions.broadcastAll(ws, [data as unknown as Tx])
}
res.end()
})
.catch((err) => {
ctx.error('JSON parse error', { err })
res.writeHead(400, {})
res.end()
})
} else {
res.writeHead(404, {})
res.end()
}
const ws = req.query.workspace as WorkspaceUuid
// push the data to body
void retrieveJson(req)
.then((data) => {
if (Array.isArray(data)) {
sessions.broadcastAll(ws, data as Tx[])
} else {
sessions.broadcastAll(ws, [data as unknown as Tx])
}
res.end()
})
.catch((err) => {
ctx.error('JSON parse error', { err })
res.writeHead(400, {})
res.end()
})
} catch (err: any) {
Analytics.handleError(err)
ctx.error('error', { err })
+5 -14
View File
@@ -347,10 +347,6 @@ describe('account utils', () => {
})
describe('getEndpoint', () => {
const mockCtx = {
error: jest.fn()
} as unknown as MeasureContext
beforeEach(() => {
jest.clearAllMocks()
})
@@ -392,8 +388,7 @@ describe('account utils', () => {
'should handle workspace="%s" region="%s" kind=%s (%s)',
(workspace, region, kind, transactors, expected, description) => {
;(getMetadata as jest.Mock).mockReturnValue(transactors)
expect(getEndpoint(mockCtx, workspace, region, kind)).toBe(expected)
expect(mockCtx.error).not.toHaveBeenCalled()
expect(getEndpoint(workspace as WorkspaceUuid, region, kind)).toBe(expected)
}
)
@@ -401,21 +396,17 @@ describe('account utils', () => {
const transactors = 'http://internal:3000;http://external:3000;'
;(getMetadata as jest.Mock).mockReturnValue(transactors)
expect(getEndpoint(mockCtx, 'workspace1', 'nonexistent', EndpointKind.Internal)).toBe('http://internal:3000')
expect(mockCtx.error).toHaveBeenCalledWith('No transactors for the target region, will use default region', {
group: 'nonexistent'
})
expect(getEndpoint('workspace1' as WorkspaceUuid, 'nonexistent', EndpointKind.Internal)).toBe(
'http://internal:3000'
)
})
test('should throw error when no transactors available', () => {
;(getMetadata as jest.Mock).mockReturnValue('http://internal:3000;http://external:3000;us')
expect(() => getEndpoint(mockCtx, 'workspace1', 'nonexistent', EndpointKind.Internal)).toThrow(
expect(() => getEndpoint('workspace1' as WorkspaceUuid, 'nonexistent', EndpointKind.Internal)).toThrow(
'Please provide transactor endpoint url'
)
expect(mockCtx.error).toHaveBeenCalledWith('No transactors for the default region')
})
})
+38 -27
View File
@@ -12,7 +12,17 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { UUID } from 'mongodb'
import {
type AccountRole,
type Data,
type Person,
type Version,
type WorkspaceMemberInfo,
type WorkspaceUuid,
AccountUuid,
buildSocialIdString,
SocialKey
} from '@hcengineering/core'
import type {
Collection,
CreateIndexesOptions,
@@ -22,38 +32,28 @@ import type {
OptionalUnlessRequiredId,
Sort as RawSort
} from 'mongodb'
import {
type Person,
type WorkspaceMemberInfo,
buildSocialIdString,
SocialKey,
type AccountRole,
type Data,
type Version,
type WorkspaceUuid,
AccountUuid
} from '@hcengineering/core'
import { UUID } from 'mongodb'
import type {
DbCollection,
Query,
Operations,
WorkspaceOperation,
AccountDB,
Account,
SocialId,
WorkspaceInvite,
OTP,
WorkspaceStatus,
AccountDB,
AccountEvent,
WorkspaceData,
WorkspaceInfoWithStatus,
WorkspaceStatusData,
Sort,
DbCollection,
Integration,
IntegrationSecret,
Mailbox,
MailboxSecret,
Integration,
IntegrationSecret
Operations,
OTP,
Query,
SocialId,
Sort,
WorkspaceData,
WorkspaceInfoWithStatus,
WorkspaceInvite,
WorkspaceOperation,
WorkspaceStatus,
WorkspaceStatusData
} from '../types'
import { isShallowEqual } from '../utils'
@@ -716,6 +716,17 @@ export class MongoAccountDB implements AccountDB {
return assignment?.role ?? null
}
async getWorkspaceRoles (accountId: AccountUuid): Promise<Map<WorkspaceUuid, AccountRole | null>> {
const assignment = await this.workspaceMembers.find({
accountUuid: accountId
})
return assignment.reduce<Map<WorkspaceUuid, AccountRole | null>>((acc, it) => {
acc.set(it.workspaceUuid, it.role)
return acc
}, new Map())
}
async getWorkspaceMembers (workspaceId: WorkspaceUuid): Promise<WorkspaceMemberInfo[]> {
return (await this.workspaceMembers.find({ workspaceUuid: workspaceId })).map((wmi) => ({
person: wmi.accountUuid,
+8 -3
View File
@@ -490,8 +490,6 @@ export class PostgresAccountDB implements AccountDB {
if (res.count === 1) {
console.log(`Applying migration: ${name}`)
await client.unsafe(ddl)
} else {
console.log(`Migration ${name} already applied`)
}
})
}
@@ -535,12 +533,19 @@ export class PostgresAccountDB implements AccountDB {
}
async getWorkspaceRole (accountUuid: AccountUuid, workspaceUuid: WorkspaceUuid): Promise<AccountRole | null> {
const res: any = await this
const res = await this
.client`SELECT role FROM ${this.client(this.getWsMembersTableName())} WHERE workspace_uuid = ${workspaceUuid} AND account_uuid = ${accountUuid}`
return res[0]?.role ?? null
}
async getWorkspaceRoles (accountUuid: AccountUuid): Promise<Map<WorkspaceUuid, AccountRole | null>> {
const res = await this
.client`SELECT workspace_uuid, role FROM ${this.client(this.getWsMembersTableName())} WHERE account_uuid = ${accountUuid}`
return new Map(res.map((it) => [it.workspace_uuid as WorkspaceUuid, it.role]))
}
async getWorkspaceMembers (workspaceUuid: WorkspaceUuid): Promise<WorkspaceMemberInfo[]> {
const res: any = await this
.client`SELECT account_uuid, role FROM ${this.client(this.getWsMembersTableName())} WHERE workspace_uuid = ${workspaceUuid}`
+111 -11
View File
@@ -16,6 +16,7 @@ import { Analytics } from '@hcengineering/analytics'
import {
AccountInfo,
AccountRole,
type AccountUuid,
type Branding,
buildSocialIdString,
concatLink,
@@ -28,18 +29,19 @@ import {
SocialIdType,
systemAccountUuid,
type WorkspaceMemberInfo,
type WorkspaceUuid,
type AccountUuid
type WorkspaceUuid
} from '@hcengineering/core'
import platform, { getMetadata, PlatformError, Severity, Status, translate } from '@hcengineering/platform'
import { decodeTokenVerbose, generateToken } from '@hcengineering/server-token'
import { isAdminEmail } from './admin'
import { accountPlugin } from './plugin'
import { type AccountServiceMethods, getServiceMethods } from './serviceOperations'
import type {
AccountDB,
AccountMethodHandler,
LoginInfo,
LoginInfoWithWorkspaces,
Mailbox,
MailboxOptions,
Meta,
@@ -55,6 +57,7 @@ import {
checkInvite,
cleanEmail,
confirmEmail,
confirmHulyIds,
createAccount,
createWorkspaceRecord,
doJoinByInvite,
@@ -63,6 +66,7 @@ import {
getAccount,
getEmailSocialId,
getEndpoint,
getEndpointInfo,
getFrontUrl,
getInviteEmail,
getMailUrl,
@@ -70,9 +74,12 @@ import {
getRegions,
getRolePower,
getWorkspaceById,
getWorkspaceByUrl,
getWorkspaceEndpoint,
getWorkspaceInfoWithStatusById,
getWorkspaceInvite,
getWorkspaceRole,
getWorkspaceRoles,
GUEST_ACCOUNT,
isEmail,
isOtpValid,
@@ -89,11 +96,8 @@ import {
verifyAllowedRole,
verifyAllowedServices,
verifyPassword,
wrap,
getWorkspaceByUrl,
confirmHulyIds
wrap
} from './utils'
import { type AccountServiceMethods, getServiceMethods } from './serviceOperations'
// Note: it is IMPORTANT to always destructure params passed here to avoid sending extra params
// to the database layer when searching/inserting as they may contain SQL injection
@@ -383,7 +387,7 @@ export async function createWorkspace (
socialId: socialId._id,
name: getPersonName(person),
token: generateToken(account, workspaceUuid),
endpoint: getEndpoint(ctx, workspaceUuid, region, EndpointKind.External),
endpoint: getEndpoint(workspaceUuid, region, EndpointKind.External),
workspace: workspaceUuid,
workspaceUrl,
role: AccountRole.Owner
@@ -1326,7 +1330,7 @@ export async function getLoginInfoByToken (
return {
...loginInfo,
workspace: workspaceUuid,
endpoint: getEndpoint(ctx, workspace.uuid, workspace.region, EndpointKind.External),
endpoint: getEndpoint(workspace.uuid, workspace.region, EndpointKind.External),
role: AccountRole.DocGuest
}
}
@@ -1342,7 +1346,7 @@ export async function getLoginInfoByToken (
...loginInfo,
workspace: workspace.uuid,
workspaceDataId: workspace.dataId,
endpoint: getEndpoint(ctx, workspace.uuid, workspace.region, EndpointKind.External),
endpoint: getEndpoint(workspace.uuid, workspace.region, EndpointKind.External),
role
}
} else {
@@ -1350,6 +1354,101 @@ export async function getLoginInfoByToken (
}
}
/**
* Validates the token and returns the decoded account information.
*/
export async function getLoginWithWorkspaceInfo (
ctx: MeasureContext,
db: AccountDB,
branding: Branding | null,
token: string
): Promise<LoginInfoWithWorkspaces> {
let accountUuid: AccountUuid
let extra: any
try {
;({ account: accountUuid, extra } = decodeTokenVerbose(ctx, token))
} catch (err: any) {
Analytics.handleError(err)
ctx.error('Invalid token', { token })
throw new PlatformError(new Status(Severity.ERROR, platform.status.Unauthorized, {}))
}
if (accountUuid == null) {
throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotFound, { account: accountUuid }))
}
const isDocGuest = accountUuid === GUEST_ACCOUNT && extra?.guest === 'true'
const isSystem = accountUuid === systemAccountUuid
let socialIds: SocialId[] = []
if (!isDocGuest && !isSystem) {
// Any confirmed social ID will do
socialIds = await db.socialId.find({ personUuid: accountUuid, verifiedOn: { $gt: 0 } })
if (socialIds.length === 0) {
return {
account: accountUuid,
workspaces: {},
socialIds: []
}
}
}
let person: Person | null
if (isDocGuest) {
person = {
uuid: accountUuid,
firstName: 'Guest',
lastName: 'User'
}
} else if (isSystem) {
person = {
uuid: accountUuid,
firstName: 'System',
lastName: 'User'
}
} else {
person = await db.person.findOne({ uuid: accountUuid })
}
if (person == null) {
throw new PlatformError(new Status(Severity.ERROR, platform.status.InternalServerError, {}))
}
const userWorkspaces = (await db.getAccountWorkspaces(accountUuid)).filter((it) => isActiveMode(it.status.mode))
const roles: Map<WorkspaceUuid, AccountRole | null> = await getWorkspaceRoles(db, accountUuid)
const info = getEndpointInfo()
const loginInfo: LoginInfoWithWorkspaces = {
account: accountUuid,
name: getPersonName(person),
socialId: socialIds[0]?._id,
token,
workspaces: Object.fromEntries(
isSystem || isDocGuest
? []
: userWorkspaces.map((it, idx) => [
it.uuid,
{
url: it.url,
dataId: it.dataId,
mode: it.status.mode,
endpoint: getWorkspaceEndpoint(info, it.uuid, it.region),
role: roles.get(it.uuid) ?? null,
version: {
versionMajor: it.status.versionMajor,
versionMinor: it.status.versionMinor,
versionPatch: it.status.versionPatch
},
progress: it.status.processingProgress
}
])
),
socialIds
}
return loginInfo
}
export async function getSocialIds (
ctx: MeasureContext,
db: AccountDB,
@@ -1650,7 +1749,6 @@ async function exchangeGuestToken (
if (tokenObj.account == null) {
// Check if it's old guest token
const oldGuestEmail = '#guest@hc.engineering'
const guestAccount = 'b6996120-416f-49cd-841e-e4a5d2e49c9b' as PersonUuid
const { linkId, guest, email, workspace: workspaceUrl } = tokenObj as any
if (linkId == null || guest == null || email !== oldGuestEmail || workspaceUrl == null) {
@@ -1663,7 +1761,7 @@ async function exchangeGuestToken (
throw new PlatformError(new Status(Severity.ERROR, platform.status.WorkspaceNotFound, { workspaceUrl }))
}
return generateToken(guestAccount, workspace.uuid, { linkId, guest: 'true' })
return generateToken(GUEST_ACCOUNT as PersonUuid, workspace.uuid, { linkId, guest: 'true' })
}
return token
@@ -1699,6 +1797,7 @@ export type AccountMethods =
| 'getWorkspaceInfo'
| 'getWorkspacesInfo'
| 'getLoginInfoByToken'
| 'getLoginWithWorkspaceInfo'
| 'getSocialIds'
| 'getPerson'
| 'getWorkspaceMembers'
@@ -1759,6 +1858,7 @@ export function getMethods (hasSignUp: boolean = true): Partial<Record<AccountMe
getWorkspaceInfo: wrap(getWorkspaceInfo),
getWorkspacesInfo: wrap(getWorkspacesInfo),
getLoginInfoByToken: wrap(getLoginInfoByToken),
getLoginWithWorkspaceInfo: wrap(getLoginWithWorkspaceInfo),
getSocialIds: wrap(getSocialIds),
getPerson: wrap(getPerson),
findPersonBySocialKey: wrap(findPersonBySocialKey),
+34 -12
View File
@@ -14,22 +14,23 @@
//
import {
AccountRole,
type Person,
BackupStatus,
Branding,
Data,
MeasureContext,
Timestamp,
Version,
WorkspaceMode,
WorkspaceMemberInfo,
BackupStatus,
type SocialId as SocialIdBase,
type PersonUuid,
type WorkspaceUuid,
type WorkspaceDataId,
WorkspaceMode,
type AccountUuid,
type Person,
type PersonId,
type AccountUuid
type PersonUuid,
type SocialId as SocialIdBase,
type WorkspaceDataId,
type WorkspaceUuid
} from '@hcengineering/core'
import type { EndpointInfo } from './utils'
/* ========= D A T A B A S E E N T I T I E S ========= */
export enum Location {
@@ -77,13 +78,16 @@ export interface Member {
role: AccountRole
}
export interface WorkspaceStatus {
workspaceUuid: WorkspaceUuid
mode: WorkspaceMode
processingProgress?: number
export interface WorkspaceVersion {
versionMajor: number
versionMinor: number
versionPatch: number
}
export interface WorkspaceStatus extends WorkspaceVersion {
workspaceUuid: WorkspaceUuid
mode: WorkspaceMode
processingProgress?: number
lastProcessingTime?: Timestamp
lastVisit?: Timestamp
isDisabled: boolean
@@ -196,6 +200,7 @@ export interface AccountDB {
updateWorkspaceRole: (accountId: AccountUuid, workspaceId: WorkspaceUuid, role: AccountRole) => Promise<void>
unassignWorkspace: (accountId: AccountUuid, workspaceId: WorkspaceUuid) => Promise<void>
getWorkspaceRole: (accountId: AccountUuid, workspaceId: WorkspaceUuid) => Promise<AccountRole | null>
getWorkspaceRoles: (accountId: AccountUuid) => Promise<Map<WorkspaceUuid, AccountRole | null>>
getWorkspaceMembers: (workspaceId: WorkspaceUuid) => Promise<WorkspaceMemberInfo[]>
getAccountWorkspaces: (accountId: AccountUuid) => Promise<WorkspaceInfoWithStatus[]>
getPendingWorkspace: (
@@ -276,6 +281,23 @@ export interface LoginInfo {
token?: string
}
export interface LoginInfoWorkspace {
url: string
dataId?: WorkspaceDataId
mode: WorkspaceMode
version: WorkspaceVersion
endpoint: EndpointInfo
role: AccountRole | null
progress?: number
}
export interface LoginInfoWithWorkspaces extends LoginInfo {
// Information necessary to handle user <--> transactor connectivity.
workspaces: Record<WorkspaceUuid, LoginInfoWorkspace>
socialIds: SocialId[]
}
export interface WorkspaceLoginInfo extends LoginInfo {
workspace: WorkspaceUuid
workspaceUrl: string
+62 -42
View File
@@ -13,54 +13,54 @@
// limitations under the License.
//
import {
AccountRole,
AccountUuid,
Branding,
concatLink,
generateId,
groupByArray,
isActiveMode,
MeasureContext,
AccountRole,
roleOrder,
SocialIdType,
WorkspaceUuid,
WorkspaceMode,
SocialKey,
systemAccountUuid,
type WorkspaceInfoWithStatus as WorkspaceInfoWithStatusCore,
isActiveMode,
type PersonUuid,
type PersonId,
WorkspaceMode,
WorkspaceUuid,
type Person,
AccountUuid
type PersonId,
type PersonUuid,
type WorkspaceInfoWithStatus as WorkspaceInfoWithStatusCore
} 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'
import { getDBClient } from '@hcengineering/postgres'
import otpGenerator from 'otp-generator'
import { pbkdf2Sync, randomBytes } from 'crypto'
import otpGenerator from 'otp-generator'
import { Analytics } from '@hcengineering/analytics'
import { sharedPipelineContextVars } from '@hcengineering/server-pipeline'
import { decodeTokenVerbose, generateToken, TokenError } from '@hcengineering/server-token'
import { MongoAccountDB } from './collections/mongo'
import { PostgresAccountDB } from './collections/postgres'
import { accountPlugin } from './plugin'
import { sharedPipelineContextVars } from '@hcengineering/server-pipeline'
import {
AccountEventType,
AccountMethodHandler,
Integration,
LoginInfo,
Meta,
OtpInfo,
WorkspaceInvite,
WorkspaceInfoWithStatus,
WorkspaceInvite,
WorkspaceLoginInfo,
WorkspaceStatus,
type Account,
type AccountDB,
type RegionInfo,
type SocialId,
type Workspace,
LoginInfo,
WorkspaceLoginInfo,
WorkspaceStatus,
AccountEventType,
Meta,
Integration
type Workspace
} from './types'
import { Analytics } from '@hcengineering/analytics'
import { TokenError, decodeTokenVerbose, generateToken } from '@hcengineering/server-token'
export const GUEST_ACCOUNT = 'b6996120-416f-49cd-841e-e4a5d2e49c9b'
@@ -172,7 +172,7 @@ export enum EndpointKind {
External
}
const toTransactor = (line: string): { internalUrl: string, region: string, externalUrl: string } => {
const toTransactor = (line: string): EndpointInfo => {
const [internalUrl, externalUrl, region] = line
.split(';')
.map((it) => it.trim())
@@ -236,33 +236,46 @@ export const _getRegions = (): RegionInfo[] => {
return _regionInfo
}
export const getEndpoint = (
ctx: MeasureContext,
workspace: string,
region: string | undefined,
kind: EndpointKind
): string => {
const byRegions = groupByArray(getEndpoints().map(toTransactor), (it) => it.region)
let transactors = (byRegions.get(region ?? '') ?? [])
.map((it) => (kind === EndpointKind.Internal ? it.internalUrl : it.externalUrl))
.flat()
export interface EndpointInfo {
internalUrl: string
externalUrl: string
region: string
}
export function getEndpointInfo (): Map<string, EndpointInfo[]> {
return groupByArray(getEndpoints().map(toTransactor), (it) => it.region)
}
export const selectKind = (kind: EndpointKind, it: EndpointInfo): string => {
return kind === EndpointKind.Internal ? it.internalUrl : it.externalUrl
}
export const getEndpoint = (workspace: WorkspaceUuid, region: string | undefined, kind: EndpointKind): string => {
const hash = hashWorkspace(workspace)
const _endpointInfo = getEndpointInfo()
let transactors = _endpointInfo.get(region ?? '') ?? []
// This is really bad
if (transactors.length === 0) {
ctx.error('No transactors for the target region, will use default region', { group: region })
transactors = (byRegions.get('') ?? [])
.map((it) => (kind === EndpointKind.Internal ? it.internalUrl : it.externalUrl))
.flat()
console.warn('No transactors for the target region, will use default region', { group: region })
transactors = _endpointInfo.get('') ?? []
}
if (transactors.length === 0) {
ctx.error('No transactors for the default region')
throw new Error('Please provide transactor endpoint url')
}
return selectKind(kind, transactors[Math.abs(hash % transactors.length)])
}
export const getWorkspaceEndpoint = (
info: Map<string, EndpointInfo[]>,
workspace: WorkspaceUuid,
region: string | undefined
): EndpointInfo => {
const hash = hashWorkspace(workspace)
return transactors[Math.abs(hash % transactors.length)]
const byRegion = info.get(region ?? '') ?? []
return byRegion[Math.abs(hash % byRegion.length)]
}
export function getAllTransactors (kind: EndpointKind): string[] {
@@ -541,7 +554,7 @@ export async function selectWorkspace (
// Guest mode select workspace
return {
account: accountUuid,
endpoint: getEndpoint(ctx, workspace.uuid, workspace.region, getKind(workspace.region)),
endpoint: getEndpoint(workspace.uuid, workspace.region, getKind(workspace.region)),
token,
workspace: workspace.uuid,
workspaceUrl: workspace.url,
@@ -572,7 +585,7 @@ export async function selectWorkspace (
return {
account: accountUuid,
token: generateToken(accountUuid, workspace.uuid, extra),
endpoint: getEndpoint(ctx, workspace.uuid, workspace.region, getKind(workspace.region)),
endpoint: getEndpoint(workspace.uuid, workspace.region, getKind(workspace.region)),
workspace: workspace.uuid,
workspaceUrl: workspace.url,
role: AccountRole.Owner
@@ -604,7 +617,7 @@ export async function selectWorkspace (
return {
account: accountUuid,
token: generateToken(accountUuid, workspace.uuid, extra),
endpoint: getEndpoint(ctx, workspace.uuid, workspace.region, getKind(workspace.region)),
endpoint: getEndpoint(workspace.uuid, workspace.region, getKind(workspace.region)),
workspace: workspace.uuid,
workspaceUrl: workspace.url,
workspaceDataId: workspace.dataId,
@@ -1413,6 +1426,13 @@ export async function getWorkspaceRole (
return await db.getWorkspaceRole(account, workspace)
}
export async function getWorkspaceRoles (
db: AccountDB,
account: AccountUuid
): Promise<Map<WorkspaceUuid, AccountRole | null>> {
return await db.getWorkspaceRoles(account)
}
export function generatePassword (len: number = 24): string {
return randomBytes(len).toString('base64').slice(0, len)
}
+2 -2
View File
@@ -332,7 +332,7 @@ class BackupWorker {
},
getConnection: async () => {
if (pipeline === undefined) {
pipeline = await this.pipelineFactory(ctx, wsIds, true, () => {}, null, null)
pipeline = await this.pipelineFactory(ctx, wsIds, () => {}, null, null)
}
return wrapPipeline(ctx, pipeline, wsIds)
},
@@ -484,7 +484,7 @@ export async function doRestoreWorkspace (
cleanIndexState,
getConnection: async () => {
if (pipeline === undefined) {
pipeline = await pipelineFactory(ctx, wsIds, true, () => {}, null, null)
pipeline = await pipelineFactory(ctx, wsIds, () => {}, null, null)
}
return wrapPipeline(ctx, pipeline, wsIds)
},
+2 -2
View File
@@ -81,7 +81,7 @@ export function initStatisticsContext (
logFile?: string
logConsole?: boolean
factory?: () => MeasureMetricsContext
getUsers?: () => WorkspaceStatistics[]
getStats?: () => WorkspaceStatistics[]
statsUrl?: string
serviceName?: () => string
}
@@ -144,7 +144,7 @@ export function initStatisticsContext (
cpu: getCPUInfo(),
memory: getMemoryInfo(),
stats: metricsContext.metrics,
workspaces: ops?.getUsers?.()
workspaces: ops?.getStats?.()
}
const statData = JSON.stringify(data)
+12 -52
View File
@@ -46,12 +46,12 @@ import {
type SearchQuery,
type SearchResult,
type SessionData,
type SocialId,
type Space,
type Timestamp,
type Tx,
type TxFactory,
type TxResult,
type WorkspaceDataId,
type WorkspaceIds,
type WorkspaceUuid
} from '@hcengineering/core'
@@ -62,7 +62,7 @@ import type { Token } from '@hcengineering/server-token'
import { type Readable } from 'stream'
import type { DbAdapter, DomainHelper } from './adapter'
import type { StatisticsElement } from './stats'
import type { StatisticsElement, WorkspaceStatistics } from './stats'
import { type StorageAdapter } from './storage'
import { type PlatformQueue } from './queue'
@@ -239,7 +239,6 @@ export interface Pipeline {
export type PipelineFactory = (
ctx: MeasureContext,
ws: WorkspaceIds,
upgrade: boolean,
broadcast: BroadcastFunc,
branding: Branding | null,
communicationApi: CommunicationApi | null
@@ -559,7 +558,7 @@ export interface ClientSessionCtx {
* @public
*/
export interface Session {
workspace: Workspace
workspace: WorkspaceIds
createTime: number
// Session restore information
@@ -587,8 +586,11 @@ export interface Session {
// Client methods
ping: (ctx: ClientSessionCtx) => Promise<void>
getUser: () => AccountUuid
getUserSocialIds: () => PersonId[]
getSocialIds: () => SocialId[]
loadModel: (ctx: ClientSessionCtx, lastModelTx: Timestamp, hash?: string) => Promise<void>
loadModelRaw: (ctx: ClientSessionCtx, lastModelTx: Timestamp, hash?: string) => Promise<LoadModelResponse | Tx[]>
getRawAccount: () => Account
@@ -663,61 +665,25 @@ export function disableLogging (): void {
LOGGING_ENABLED = false
}
interface TickHandler {
ticks: number
operation: () => void
}
/**
* @public
*/
export interface Workspace {
context: MeasureContext
id: string
token: string // Account workspace update token.
pipeline: Promise<Pipeline> | Pipeline
communicationApi: Promise<CommunicationApi> | CommunicationApi
tickHash: number
tickHandlers: Map<string, TickHandler>
sessions: Map<string, { session: Session, socket: ConnectionSocket, tickHash: number }>
upgrade: boolean
closing?: Promise<void>
softShutdown: number
workspaceInitCompleted: boolean
workspaceName: string
workspaceUuid: WorkspaceUuid
workspaceUrl: string
workspaceDataId?: WorkspaceDataId
branding: Branding | null
}
export interface AddSessionActive {
session: Session
context: MeasureContext
workspaceId: WorkspaceUuid
}
export type AddSessionResponse =
| AddSessionActive
export type GetWorkspaceResponse =
| { upgrade: true, progress?: number }
| { error: any, terminate?: boolean, specialError?: 'archived' | 'migration' }
export type SessionFactory = (token: Token, workspace: Workspace, account: Account) => Session
export type AddSessionResponse = AddSessionActive | GetWorkspaceResponse
/**
* @public
*/
export interface SessionManager {
workspaces: Map<WorkspaceUuid, Workspace>
// workspaces: Map<WorkspaceUuid, Workspace>
sessions: Map<string, { session: Session, socket: ConnectionSocket }>
createSession: SessionFactory
addSession: (
ctx: MeasureContext,
ws: ConnectionSocket,
@@ -726,18 +692,10 @@ export interface SessionManager {
sessionId: string | undefined
) => Promise<AddSessionResponse>
broadcastAll: (workspace: Workspace, tx: Tx[], targets?: string[]) => void
broadcastAll: (workspace: WorkspaceUuid, tx: Tx[], targets?: string[]) => void
close: (ctx: MeasureContext, ws: ConnectionSocket, workspaceId: WorkspaceUuid) => Promise<void>
closeAll: (
wsId: WorkspaceUuid,
workspace: Workspace,
code: number,
reason: 'upgrade' | 'shutdown',
ignoreSocket?: ConnectionSocket
) => Promise<void>
forceClose: (wsId: WorkspaceUuid, ignoreSocket?: ConnectionSocket) => Promise<void>
closeWorkspaces: (ctx: MeasureContext) => Promise<void>
@@ -773,6 +731,8 @@ export interface SessionManager {
service: Session,
ws: ConnectionSocket
) => ClientSessionCtx
getStatistics: () => WorkspaceStatistics[]
}
export const pingConst = 'ping'
-3
View File
@@ -7,7 +7,6 @@ import core, {
type Account,
type AccountUuid,
type BackupClient,
type Branding,
type BrandingMap,
type BulkUpdateEvent,
type Class,
@@ -166,7 +165,6 @@ export class SessionDataImpl implements SessionData {
readonly admin: boolean | undefined,
_broadcast: SessionData['broadcast'] | undefined,
readonly workspace: WorkspaceIds,
readonly branding: Branding | null,
readonly isAsyncContext: boolean,
_removedMap: Map<Ref<Doc>, Doc> | undefined,
_contextCache: Map<string, any> | undefined,
@@ -244,7 +242,6 @@ export function wrapPipeline (
true,
{ targets: {}, txes: [] },
wsIds,
null,
true,
undefined,
undefined,
-1
View File
@@ -559,7 +559,6 @@ export class FullTextIndexPipeline implements FullTextPipeline {
true,
undefined,
this.workspace,
null,
false,
undefined,
undefined,
-1
View File
@@ -221,7 +221,6 @@ export class TriggersMiddleware extends BaseMiddleware implements Middleware {
sctx.admin,
{ txes: [], targets: {} },
this.context.workspace,
this.context.branding,
true,
sctx.removedMap,
sctx.contextCache,
+3 -3
View File
@@ -110,7 +110,7 @@ export function createServerPipeline (
},
extensions?: Partial<DbConfiguration>
): PipelineFactory {
return (ctx, workspace, upgrade, broadcast, branding, communicationApi) => {
return (ctx, workspace, broadcast, branding, communicationApi) => {
const metricsCtx = opt.usePassedCtx === true ? ctx : metrics
const wsMetrics = metricsCtx.newChild('🧲 session', {})
const conf = getConfig(metrics, dbUrl, wsMetrics, opt, extensions)
@@ -176,7 +176,7 @@ export function createBackupPipeline (
externalStorage: StorageAdapter
}
): PipelineFactory {
return (ctx, workspace, upgrade, broadcast, branding, communicationApi) => {
return (ctx, workspace, broadcast, branding, communicationApi) => {
const metricsCtx = opt.usePassedCtx === true ? ctx : metrics
const wsMetrics = metricsCtx.newChild('🧲 backup', {})
const conf = getConfig(metrics, dbUrl, wsMetrics, {
@@ -229,7 +229,7 @@ export async function getServerPipeline (
})
// TODO: Communication API ??
return await pipelineFactory(ctx, wsUrl, true, () => {}, null, null)
return await pipelineFactory(ctx, wsUrl, () => {}, null, null)
}
const txAdapterFactories: Record<string, DbAdapterFactory> = {}
+2 -1
View File
@@ -47,6 +47,7 @@
"@hcengineering/platform": "^0.6.11",
"@hcengineering/rpc": "^0.6.5",
"@hcengineering/server-core": "^0.6.1",
"@hcengineering/server-token": "^0.6.11"
"@hcengineering/server-token": "^0.6.11",
"utf-8-validate": "^6.0.4"
}
}
+27 -22
View File
@@ -13,6 +13,21 @@
// limitations under the License.
//
import type { LoginInfoWithWorkspaces } from '@hcengineering/account-client'
import {
RequestEvent as CommunicationEvent,
SessionData as CommunicationSession,
EventResult
} from '@hcengineering/communication-sdk-types'
import {
FindLabelsParams,
FindMessagesGroupsParams,
FindMessagesParams,
FindNotificationContextParams,
FindNotificationsParams,
Message,
MessagesGroup
} from '@hcengineering/communication-types'
import {
AccountUuid,
generateId,
@@ -32,11 +47,13 @@ import {
type SearchQuery,
type SearchResult,
type SessionData,
type SocialId,
type Timestamp,
type Tx,
type TxCUD,
type TxResult,
type WorkspaceDataId
type WorkspaceDataId,
type WorkspaceIds
} from '@hcengineering/core'
import { PlatformError, unknownError } from '@hcengineering/platform'
import {
@@ -48,24 +65,9 @@ import {
type Pipeline,
type Session,
type SessionRequest,
type StatisticsElement,
type Workspace
type StatisticsElement
} from '@hcengineering/server-core'
import { type Token } from '@hcengineering/server-token'
import {
FindMessagesGroupsParams,
FindMessagesParams,
Message,
MessagesGroup,
FindNotificationContextParams,
FindNotificationsParams,
FindLabelsParams
} from '@hcengineering/communication-types'
import {
RequestEvent as CommunicationEvent,
SessionData as CommunicationSession,
EventResult
} from '@hcengineering/communication-sdk-types'
const useReserveContext = (process.env.USE_RESERVE_CTX ?? 'true') === 'true'
@@ -93,8 +95,9 @@ export class ClientSession implements Session {
constructor (
protected readonly token: Token,
readonly workspace: Workspace,
readonly workspace: WorkspaceIds,
readonly account: Account,
readonly info: LoginInfoWithWorkspaces,
readonly allowUpload: boolean
) {
this.isAdmin = this.token.extra?.admin === 'true'
@@ -108,6 +111,10 @@ export class ClientSession implements Session {
return this.account.socialIds
}
getSocialIds (): SocialId[] {
return this.info.socialIds
}
getRawAccount (): Account {
return this.account
}
@@ -142,18 +149,16 @@ export class ClientSession implements Session {
}
includeSessionContext (ctx: ClientSessionCtx): void {
const dataId = this.workspace.workspaceDataId ?? (this.workspace.workspaceUuid as unknown as WorkspaceDataId)
const dataId = this.workspace.dataId ?? (this.workspace.uuid as unknown as WorkspaceDataId)
const contextData = new SessionDataImpl(
this.account,
this.sessionId,
this.isAdmin,
undefined,
{
uuid: this.workspace.workspaceUuid,
url: this.workspace.workspaceUrl,
...this.workspace,
dataId
},
this.workspace.branding,
false,
undefined,
undefined,
File diff suppressed because it is too large Load Diff
+2 -16
View File
@@ -20,22 +20,8 @@ export function getStatistics (ctx: MeasureContext, sessions: SessionManager, ad
}
data.statistics.totalClients = sessions.sessions.size
if (admin) {
for (const [k, vv] of sessions.workspaces) {
data.statistics.activeSessions[k] = {
sessions: Array.from(vv.sessions.entries()).map(([k, v]) => ({
userId: v.session.getUser(),
data: v.socket.data(),
mins5: v.session.mins5,
total: v.session.total,
current: v.session.current,
upgrade: v.session.isUpgradeClient()
})),
name: vv.workspaceName,
wsId: vv.workspaceUuid,
sessionsTotal: vv.sessions.size,
upgrading: vv.upgrade,
closing: vv.closing !== undefined
}
for (const wsStats of sessions.getStatistics()) {
data.statistics.activeSessions[wsStats.wsId] = wsStats
}
}
+130
View File
@@ -0,0 +1,130 @@
//
// Copyright © 2022, 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.
//
import { Analytics } from '@hcengineering/analytics'
import { type ServerApi as CommunicationApi } from '@hcengineering/communication-sdk-types'
import { type Branding, type MeasureContext, type WorkspaceIds } from '@hcengineering/core'
import type { ConnectionSocket, Pipeline, Session } from '@hcengineering/server-core'
interface TickHandler {
ticks: number
operation: () => void
}
export interface PipelinePair {
pipeline: Pipeline
communicationApi: CommunicationApi
}
export type WorkspacePipelineFactory = () => Promise<PipelinePair>
/**
* @public
*/
export class Workspace {
pipeline?: PipelinePair | Promise<PipelinePair>
upgrade: boolean = false
closing?: Promise<void>
workspaceInitCompleted: boolean = false
softShutdown: number
sessions = new Map<string, { session: Session, socket: ConnectionSocket, tickHash: number }>()
tickHandlers = new Map<string, TickHandler>()
operations: number = 0
constructor (
readonly context: MeasureContext,
readonly token: string, // Account workspace update token.
readonly factory: WorkspacePipelineFactory,
readonly tickHash: number,
softShutdown: number,
readonly wsId: WorkspaceIds,
readonly branding: Branding | null
) {
this.softShutdown = softShutdown
}
private getPipelinePair (): PipelinePair | Promise<PipelinePair> {
if (this.pipeline === undefined) {
this.pipeline = this.factory()
}
return this.pipeline
}
async with<T>(op: (pipeline: Pipeline, communicationApi: CommunicationApi) => Promise<T>): Promise<T> {
this.operations++
let pair = this.getPipelinePair()
if (pair instanceof Promise) {
pair = await pair
this.pipeline = pair
}
try {
return await op(pair.pipeline, pair.communicationApi)
} finally {
this.operations--
}
}
async close (ctx: MeasureContext): Promise<void> {
if (this.pipeline === undefined) {
return
}
const { pipeline, communicationApi } = await this.pipeline
const closePipeline = async (): Promise<void> => {
try {
await ctx.with('close-pipeline', {}, async () => {
await pipeline.close()
})
} catch (err: any) {
Analytics.handleError(err)
ctx.error('close-pipeline-error', { error: err })
}
}
const closeCommunicationApi = async (): Promise<void> => {
try {
await ctx.with('close-communication-api', {}, async () => {
await communicationApi.close()
})
} catch (err: any) {
Analytics.handleError(err)
ctx.error('close-pipeline-error', { error: err })
}
}
await ctx.with('closing', {}, async () => {
const to = timeoutPromise(120000)
const closePromises = [closePipeline(), closeCommunicationApi()]
await Promise.race([Promise.all(closePromises), to.promise])
to.cancelHandle()
})
}
}
function timeoutPromise (time: number): { promise: Promise<void>, cancelHandle: () => void } {
let timer: any
return {
promise: new Promise((resolve) => {
timer = setTimeout(resolve, time)
}),
cancelHandle: () => {
clearTimeout(timer)
}
}
}
@@ -318,7 +318,6 @@ export async function upgradeWorkspaceWith (
true,
undefined,
wsIds,
null,
true,
undefined,
undefined,