From 8cc1d4034de93d6bbba350a118b6f5e7a6dc3b11 Mon Sep 17 00:00:00 2001 From: NikolaDev Date: Mon, 24 Feb 2025 06:26:20 +0100 Subject: [PATCH 1/7] Fixed a typo in an IF statement (#5976) Signed-off-by: Nikola Stancic --- templates/apply.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/apply.js b/templates/apply.js index 3748c7e80c..e0a691bcef 100644 --- a/templates/apply.js +++ b/templates/apply.js @@ -126,9 +126,9 @@ function updatePackage(packageRoot, templates) { const preferedOrder = ['name', 'version', 'main', 'svelte', 'types', 'files', 'author', 'template', 'license', 'scripts', 'devDependencies', 'dependencies', 'repository', 'publishConfig'] Object.keys(currentPackage).forEach(it => { - if( !preferedOrder.includes(it)) [ + if( !preferedOrder.includes(it)) { preferedOrder.push(it) - ] + } }) const ordered = preferedOrder.reduce( From 0d76e68516ddb0cfc097b4f66bdd475555ba9ec4 Mon Sep 17 00:00:00 2001 From: Kevin Freistroffer Date: Sun, 23 Feb 2025 21:31:22 -0800 Subject: [PATCH 2/7] Update README.md (#6984) Signed-off-by: Kevin Freistroffer --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 48d5b15082..3f50824cf1 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ We periodically merge `develop` into `staging` to perform testing builds. Once w ## Installation -You need Microsoft's [rush](https://rushjs.io) to install application. +You need Microsoft's [rush](https://rushjs.io) to install the application. 1. Install Rush globally using the command: From cd90f8bce6eefa9f99adf668d48a99d6ed4acc6b Mon Sep 17 00:00:00 2001 From: Andrey Sobolev Date: Mon, 24 Feb 2025 10:26:10 +0700 Subject: [PATCH 3/7] Initial rest RPC (#8076) Signed-off-by: Andrey Sobolev --- packages/api-client/package.json | 6 +- packages/api-client/src/index.ts | 1 + packages/api-client/src/rest.ts | 130 ++++++++++ packages/core/src/measurements/metrics.ts | 26 ++ .../components/ServerManagerGeneral.svelte | 16 +- server/core/src/types.ts | 20 +- server/server/src/client.ts | 54 +++-- server/server/src/sessionManager.ts | 62 ++++- server/ws/package.json | 3 +- server/ws/src/__tests__/rest.test.ts | 225 ++++++++++++++++++ server/ws/src/rpc.ts | 182 ++++++++++++++ server/ws/src/server_http.ts | 36 ++- server/ws/src/utils.ts | 23 ++ 13 files changed, 727 insertions(+), 57 deletions(-) create mode 100644 packages/api-client/src/rest.ts create mode 100644 server/ws/src/__tests__/rest.test.ts create mode 100644 server/ws/src/rpc.ts create mode 100644 server/ws/src/utils.ts diff --git a/packages/api-client/package.json b/packages/api-client/package.json index 52baa57bd7..7666e529d9 100644 --- a/packages/api-client/package.json +++ b/packages/api-client/package.json @@ -39,7 +39,8 @@ "ts-node": "^10.8.0", "@types/node": "~20.11.16", "@types/jest": "^29.5.5", - "@types/ws": "^8.5.11" + "@types/ws": "^8.5.11", + "@types/snappyjs": "^0.7.1" }, "dependencies": { "@hcengineering/core": "^0.6.32", @@ -48,7 +49,8 @@ "@hcengineering/collaborator-client": "^0.6.4", "@hcengineering/account-client": "^0.6.0", "@hcengineering/platform": "^0.6.11", - "@hcengineering/text": "^0.6.5" + "@hcengineering/text": "^0.6.5", + "snappyjs": "^0.7.0" }, "repository": "https://github.com/hcengineering/platform", "publishConfig": { diff --git a/packages/api-client/src/index.ts b/packages/api-client/src/index.ts index 2dd51ba8f5..911f155e01 100644 --- a/packages/api-client/src/index.ts +++ b/packages/api-client/src/index.ts @@ -17,3 +17,4 @@ export * from './client' export * from './markup/types' export * from './socket' export * from './types' +export * from './rest' diff --git a/packages/api-client/src/rest.ts b/packages/api-client/src/rest.ts new file mode 100644 index 0000000000..23648ca2d1 --- /dev/null +++ b/packages/api-client/src/rest.ts @@ -0,0 +1,130 @@ +// +// 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. +// + +import { + type Account, + type Class, + type Doc, + type DocumentQuery, + type FindOptions, + type FindResult, + type Ref, + type Storage, + type Tx, + type TxResult, + type WithLookup, + concatLink +} from '@hcengineering/core' + +import { PlatformError, unknownError } from '@hcengineering/platform' + +import { uncompress } from 'snappyjs' + +export interface RestClient extends Storage { + getAccount: () => Promise + + findOne: ( + _class: Ref>, + query: DocumentQuery, + options?: FindOptions + ) => Promise | undefined> +} + +export async function createRestClient (endpoint: string, workspaceId: string, token: string): Promise { + return new RestClientImpl(endpoint, workspaceId, token) +} + +class RestClientImpl implements RestClient { + constructor ( + readonly endpoint: string, + readonly workspace: string, + readonly token: string + ) {} + + async findAll( + _class: Ref>, + query: DocumentQuery, + options?: FindOptions + ): Promise> { + const params = new URLSearchParams() + params.append('class', _class) + if (query !== undefined && Object.keys(query).length > 0) { + params.append('query', JSON.stringify(query)) + } + if (options !== undefined && Object.keys(options).length > 0) { + params.append('options', JSON.stringify(options)) + } + const response = await fetch(concatLink(this.endpoint, `/api/v1/find-all/${this.workspace}?${params.toString()}`), { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: 'Bearer ' + this.token, + 'accept-encoding': 'snappy, gzip' + }, + keepalive: true + }) + if (!response.ok) { + throw new PlatformError(unknownError(response.statusText)) + } + const encoding = response.headers.get('content-encoding') + if (encoding === 'snappy') { + const buffer = await response.arrayBuffer() + const decompressed = uncompress(buffer) + const decoder = new TextDecoder() + const jsonString = decoder.decode(decompressed) + return JSON.parse(jsonString) as FindResult + } + return (await response.json()) as FindResult + } + + async getAccount (): Promise { + const response = await fetch(concatLink(this.endpoint, `/api/v1/account/${this.workspace}`), { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: 'Bearer ' + this.token + }, + keepalive: true + }) + if (!response.ok) { + throw new PlatformError(unknownError(response.statusText)) + } + return (await response.json()) as Account + } + + async findOne( + _class: Ref>, + query: DocumentQuery, + options?: FindOptions + ): Promise | undefined> { + return (await this.findAll(_class, query, { ...options, limit: 1 })).shift() + } + + async tx (tx: Tx): Promise { + const response = await fetch(concatLink(this.endpoint, `/api/v1/tx/${this.workspace}`), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: 'Bearer ' + this.token + }, + keepalive: true, + body: JSON.stringify(tx) + }) + if (!response.ok) { + throw new PlatformError(unknownError(response.statusText)) + } + return (await response.json()) as TxResult + } +} diff --git a/packages/core/src/measurements/metrics.ts b/packages/core/src/measurements/metrics.ts index 09ab8fb7e0..0fe3267f3d 100644 --- a/packages/core/src/measurements/metrics.ts +++ b/packages/core/src/measurements/metrics.ts @@ -234,6 +234,28 @@ function toString (name: string, m: Metrics, offset: number, length: number): st return r } +function toJson (m: Metrics): any { + const obj: any = { + $total: m.value, + $ops: m.operations + } + if (m.operations > 1) { + obj.avg = Math.round((m.value / (m.operations > 0 ? m.operations : 1)) * 100) / 100 + } + if (Object.keys(m.params).length > 0) { + obj.params = m.params + } + for (const [k, v] of Object.entries(m.measurements ?? {})) { + obj[ + `${k} ${v.value} ${v.operations} ${ + v.operations > 1 ? Math.round((v.value / (v.operations > 0 ? m.operations : 1)) * 100) / 100 : '' + }` + ] = toJson(v) + } + + return obj +} + /** * @public */ @@ -241,6 +263,10 @@ export function metricsToString (metrics: Metrics, name = 'System', length: numb return toString(name, metricsAggregate(metrics, 50, true), 0, length) } +export function metricsToJson (metrics: Metrics): any { + return toJson(metricsAggregate(metrics)) +} + function printMetricsParamsRows ( params: Record>, offset: number diff --git a/plugins/workbench-resources/src/components/ServerManagerGeneral.svelte b/plugins/workbench-resources/src/components/ServerManagerGeneral.svelte index d53b5db874..4b752d0b17 100644 --- a/plugins/workbench-resources/src/components/ServerManagerGeneral.svelte +++ b/plugins/workbench-resources/src/components/ServerManagerGeneral.svelte @@ -3,8 +3,7 @@ import login from '@hcengineering/login' import { getEmbeddedLabel, getMetadata } from '@hcengineering/platform' import presentation, { getClient, isAdminUser, uiContext } from '@hcengineering/presentation' - import { Button, IconArrowLeft, IconArrowRight, fetchMetadataLocalStorage, ticker } from '@hcengineering/ui' - import EditBox from '@hcengineering/ui/src/components/EditBox.svelte' + import { Button, EditBox, IconArrowLeft, IconArrowRight, fetchMetadataLocalStorage, ticker } from '@hcengineering/ui' import MetricsInfo from './statistics/MetricsInfo.svelte' const _endpoint: string = fetchMetadataLocalStorage(login.metadata.LoginEndpoint) ?? '' @@ -25,8 +24,6 @@ let avgTime = 0 - let rps = 0 - let active = 0 let opss = 0 @@ -44,7 +41,7 @@ profiling = data?.profiling ?? false }) .catch((err) => { - console.error(err) + console.error(err, time) }) } let data: any @@ -65,13 +62,8 @@ avgTime = 0 maxTime = 0 let count = commandsToSend - let ops = 0 avgTime = 0 opss = 0 - const int = setInterval(() => { - rps = ops - ops = 0 - }, 1000) const rate = new RateLimiter(commandsToSendParallel) const client = getClient() @@ -96,7 +88,6 @@ } else { avgTime = ed - st } - ops++ opss++ count-- } @@ -112,7 +103,6 @@ } await rate.waitProcessing() running = false - clearInterval(int) } async function downloadProfile (): Promise { @@ -132,7 +122,7 @@ document.body.appendChild(link) link.click() document.body.removeChild(link) - void fetchStats(0) + await fetchStats(0) } let metrics: Metrics | undefined diff --git a/server/core/src/types.ts b/server/core/src/types.ts index 373e439fd3..4e8ceac0d2 100644 --- a/server/core/src/types.ts +++ b/server/core/src/types.ts @@ -571,6 +571,16 @@ export interface Session { ) => Promise> searchFulltext: (ctx: ClientSessionCtx, query: SearchQuery, options: SearchOptions) => Promise tx: (ctx: ClientSessionCtx, tx: Tx) => Promise + + txRaw: ( + ctx: ClientSessionCtx, + tx: Tx + ) => Promise<{ + result: TxResult + broadcastPromise: Promise + asyncsPromise: Promise | undefined + }> + loadChunk: (ctx: ClientSessionCtx, domain: Domain, idx?: number) => Promise getDomainHash: (ctx: ClientSessionCtx, domain: Domain) => Promise @@ -707,11 +717,17 @@ export interface SessionManager { createOpContext: ( ctx: MeasureContext, pipeline: Pipeline, - request: Request, + requestId: Request['id'], service: Session, ws: ConnectionSocket, - workspace: WorkspaceUuid ) => ClientSessionCtx + + handleRPC: ( + requestCtx: MeasureContext, + service: S, + ws: ConnectionSocket, + operation: (ctx: ClientSessionCtx) => Promise + ) => Promise } /** diff --git a/server/server/src/client.ts b/server/server/src/client.ts index 2ce2e1fce2..b0869d9e20 100644 --- a/server/server/src/client.ts +++ b/server/server/src/client.ts @@ -24,6 +24,8 @@ import { type FindOptions, type FindResult, type MeasureContext, + type PersonId, + type PersonUuid, type Ref, type SearchOptions, type SearchQuery, @@ -31,9 +33,8 @@ import { type Timestamp, type Tx, type TxCUD, - type PersonId, - type WorkspaceDataId, - type PersonUuid + type TxResult, + type WorkspaceDataId } from '@hcengineering/core' import { PlatformError, unknownError } from '@hcengineering/platform' import { @@ -164,7 +165,14 @@ export class ClientSession implements Session { await ctx.sendResponse(ctx.requestId, await ctx.pipeline.searchFulltext(ctx.ctx, query, options)) } - async tx (ctx: ClientSessionCtx, tx: Tx): Promise { + async txRaw ( + ctx: ClientSessionCtx, + tx: Tx + ): Promise<{ + result: TxResult + broadcastPromise: Promise + asyncsPromise: Promise | undefined + }> { this.lastRequest = Date.now() this.total.tx++ this.current.tx++ @@ -173,31 +181,45 @@ export class ClientSession implements Session { let cid = 'client_' + generateId() ctx.ctx.id = cid let onEnd = useReserveContext ? ctx.pipeline.context.adapterManager?.reserveContext?.(cid) : undefined + let result: TxResult try { - const result = await ctx.pipeline.tx(ctx.ctx, [tx]) - - // Send result immideately - await ctx.sendResponse(ctx.requestId, result) - - // We need to broadcast all collected transactions - await ctx.pipeline.handleBroadcast(ctx.ctx) + result = await ctx.pipeline.tx(ctx.ctx, [tx]) } finally { onEnd?.() } + // Send result immideately + await ctx.sendResponse(ctx.requestId, result) + + // We need to broadcast all collected transactions + const broadcastPromise = ctx.pipeline.handleBroadcast(ctx.ctx) // ok we could perform async requests if any const asyncs = (ctx.ctx.contextData as SessionData).asyncRequests ?? [] + let asyncsPromise: Promise | undefined if (asyncs.length > 0) { cid = 'client_async_' + generateId() ctx.ctx.id = cid onEnd = useReserveContext ? ctx.pipeline.context.adapterManager?.reserveContext?.(cid) : undefined - try { - for (const r of (ctx.ctx.contextData as SessionData).asyncRequests ?? []) { - await r() + const handleAyncs = async (): Promise => { + try { + for (const r of (ctx.ctx.contextData as SessionData).asyncRequests ?? []) { + await r() + } + } finally { + onEnd?.() } - } finally { - onEnd?.() } + asyncsPromise = handleAyncs() + } + + return { result, broadcastPromise, asyncsPromise } + } + + async tx (ctx: ClientSessionCtx, tx: Tx): Promise { + const { broadcastPromise, asyncsPromise } = await this.txRaw(ctx, tx) + await broadcastPromise + if (asyncsPromise !== undefined) { + await asyncsPromise } } diff --git a/server/server/src/sessionManager.ts b/server/server/src/sessionManager.ts index bbfe0eecf2..57ebea38be 100644 --- a/server/server/src/sessionManager.ts +++ b/server/server/src/sessionManager.ts @@ -91,7 +91,7 @@ export interface Timeouts { reconnectTimeout: number // Default 3 seconds } -class TSessionManager implements SessionManager { +export class TSessionManager implements SessionManager { private readonly statusPromises = new Map>() readonly workspaces = new Map() checkInterval: any @@ -981,7 +981,7 @@ class TSessionManager implements SessionManager { createOpContext ( ctx: MeasureContext, pipeline: Pipeline, - request: Request, + requestId: Request['id'], service: Session, ws: ConnectionSocket, workspace: WorkspaceUuid @@ -990,7 +990,7 @@ class TSessionManager implements SessionManager { return { ctx, pipeline, - requestId: request.id, + requestId, sendResponse: (reqId, msg) => sendResponse(ctx, service, ws, { id: reqId, @@ -1072,6 +1072,7 @@ class TSessionManager implements SessionManager { return } if (request.id === -2 && request.method === 'forceClose') { + // TODO: we chould allow this only for admin or system accounts let done = false const wsRef = this.workspaces.get(workspace) if (wsRef?.upgrade ?? false) { @@ -1106,7 +1107,7 @@ class TSessionManager implements SessionManager { const params = [...request.params] await ctx.with('🧨 process', {}, (callTx) => - f.apply(service, [this.createOpContext(callTx, pipeline, request, service, ws, workspace), ...params]) + f.apply(service, [this.createOpContext(callTx, pipeline, request.id, service, ws), ...params]) ) } catch (err: any) { Analytics.handleError(err) @@ -1131,6 +1132,59 @@ class TSessionManager implements SessionManager { }) } + handleRPC( + requestCtx: MeasureContext, + service: S, + ws: ConnectionSocket, + operation: (ctx: ClientSessionCtx) => Promise + ): Promise { + const userCtx = requestCtx.newChild('📞 client', {}) + + // Calculate total number of clients + const reqId = generateId() + + const st = Date.now() + return userCtx + .with('🧭 handleRPC', {}, async (ctx) => { + if (service.workspace.closing !== undefined) { + throw new Error('Workspace is closing') + } + + service.requests.set(reqId, { + id: reqId, + params: {}, + start: st + }) + + const pipeline = + service.workspace.pipeline instanceof Promise ? await service.workspace.pipeline : service.workspace.pipeline + + try { + const uctx = this.createOpContext(ctx, pipeline, reqId, service, ws) + await operation(uctx) + } catch (err: any) { + Analytics.handleError(err) + if (LOGGING_ENABLED) { + this.ctx.error('error handle request', { error: err }) + } + ws.send( + ctx, + { + id: reqId, + error: unknownError(err), + result: JSON.parse(JSON.stringify(err?.stack)) + }, + service.binaryMode, + service.useCompression + ) + } + }) + .finally(() => { + userCtx.end() + service.requests.delete(reqId) + }) + } + private async handleHello( request: Request, service: S, diff --git a/server/ws/package.json b/server/ws/package.json index c97852ccef..5ecf869145 100644 --- a/server/ws/package.json +++ b/server/ws/package.json @@ -53,6 +53,7 @@ "utf-8-validate": "^6.0.4", "ws": "^8.18.0", "body-parser": "^1.20.2", - "snappy": "^7.2.2" + "snappy": "^7.2.2", + "@hcengineering/api-client": "^0.6.0" } } diff --git a/server/ws/src/__tests__/rest.test.ts b/server/ws/src/__tests__/rest.test.ts new file mode 100644 index 0000000000..6ad4530733 --- /dev/null +++ b/server/ws/src/__tests__/rest.test.ts @@ -0,0 +1,225 @@ +// +// 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. +// + +import { generateToken } from '@hcengineering/server-token' + +import { createRestClient, type RestClient } from '@hcengineering/api-client' +import core, { + generateId, + getWorkspaceId, + Hierarchy, + MeasureMetricsContext, + ModelDb, + toFindResult, + type Class, + type Doc, + type DocumentQuery, + type Domain, + type FindOptions, + type FindResult, + type MeasureContext, + type Ref, + type Space, + type Tx, + type TxCreateDoc, + type TxResult +} from '@hcengineering/core' +import { ClientSession, startSessionManager, type TSessionManager } from '@hcengineering/server' +import { createDummyStorageAdapter, type SessionManager, type WorkspaceLoginInfo } from '@hcengineering/server-core' +import { startHttpServer } from '../server_http' +import { genMinModel } from './minmodel' + +describe('rest-server', () => { + async function getModelDb (): Promise<{ modelDb: ModelDb, hierarchy: Hierarchy, txes: Tx[] }> { + const txes = genMinModel() + const hierarchy = new Hierarchy() + for (const tx of txes) { + hierarchy.tx(tx) + } + const modelDb = new ModelDb(hierarchy) + for (const tx of txes) { + await modelDb.tx(tx) + } + return { modelDb, hierarchy, txes } + } + + let shutdown: () => Promise + let sessionManager: SessionManager + const port: number = 3330 + + beforeAll(async () => { + ;({ shutdown, sessionManager } = startSessionManager(new MeasureMetricsContext('test', {}), { + pipelineFactory: async () => { + const { modelDb, hierarchy, txes } = await getModelDb() + return { + hierarchy, + modelDb, + context: { + workspace: { + name: 'test-ws', + workspaceName: 'test-ws', + workspaceUrl: 'test-ws' + }, + hierarchy, + modelDb, + lastTx: generateId(), + lastHash: generateId(), + contextVars: {}, + branding: null + }, + handleBroadcast: async (ctx) => {}, + findAll: async ( + ctx: MeasureContext, + _class: Ref>, + query: DocumentQuery, + options?: FindOptions + ): Promise> => toFindResult(await modelDb.findAll(_class, query, options)), + tx: async (ctx: MeasureContext, tx: Tx[]): Promise<[TxResult, Tx[], string[] | undefined]> => [ + await modelDb.tx(...tx), + [], + undefined + ], + close: async () => {}, + domains: async () => hierarchy.domains(), + groupBy: async () => new Map(), + find: (ctx: MeasureContext, domain: Domain) => ({ + next: async (ctx: MeasureContext) => undefined, + close: async (ctx: MeasureContext) => {} + }), + load: async (ctx: MeasureContext, domain: Domain, docs: Ref[]) => [], + upload: async (ctx: MeasureContext, domain: Domain, docs: Doc[]) => {}, + clean: async (ctx: MeasureContext, domain: Domain, docs: Ref[]) => {}, + searchFulltext: async (ctx, query, options) => { + return { docs: [] } + }, + loadModel: async (ctx, lastModelTx, hash) => ({ + full: true, + hash: generateId(), + transactions: txes + }) + } + }, + sessionFactory: (token, workspace) => new ClientSession(token, workspace, true), + port, + brandingMap: {}, + serverFactory: startHttpServer, + accountsUrl: '', + externalStorage: createDummyStorageAdapter() + })) + jest + .spyOn(sessionManager as TSessionManager, 'getWorkspaceInfo') + .mockImplementation(async (ctx: MeasureContext, token: string): Promise => { + return { + workspaceId: 'test-ws', + workspaceUrl: 'test-ws', + workspaceName: 'Test Workspace', + uuid: 'test-ws', + createdBy: 'test-owner', + mode: 'active', + createdOn: Date.now(), + lastVisit: Date.now(), + disabled: false, + endpoint: `http://localhost:${port}`, + region: 'test-region', + targetRegion: 'test-region', + backupInfo: { + dataSize: 0, + blobsSize: 0, + backupSize: 0, + lastBackup: 0, + backups: 0 + } + } + }) + }) + afterAll(async () => { + await shutdown() + }) + + async function connect (): Promise { + const token: string = generateToken('user1@site.com', getWorkspaceId('test-ws')) + return await createRestClient(`http://localhost:${port}`, 'test-ws', token) + } + + it('get account', async () => { + const conn = await connect() + const account = await conn.getAccount() + + expect(account.email).toBe('user1@site.com') + expect(account.role).toBe('OWNER') + expect(account._id).toBe('User1') + expect(account._class).toBe('core:class:Account') + expect(account.space).toBe('core:space:Model') + expect(account.modifiedBy).toBe('core:account:System') + expect(account.createdBy).toBe('core:account:System') + expect(typeof account.modifiedOn).toBe('number') + expect(typeof account.createdOn).toBe('number') + }) + + it('find spaces', async () => { + const conn = await connect() + const spaces = await conn.findAll(core.class.Space, {}) + expect(spaces.length).toBe(2) + expect(spaces[0].name).toBe('Sp1') + expect(spaces[1].name).toBe('Sp2') + }) + + it('find avg', async () => { + const conn = await connect() + let ops = 0 + let total = 0 + const attempts = 1000 + for (let i = 0; i < attempts; i++) { + const st = performance.now() + const spaces = await conn.findAll(core.class.Space, {}) + expect(spaces.length).toBe(2) + expect(spaces[0].name).toBe('Sp1') + expect(spaces[1].name).toBe('Sp2') + const ed = performance.now() + ops++ + total += ed - st + } + const avg = total / ops + // console.log('ops:', ops, 'total:', total, 'avg:', ) + expect(ops).toEqual(attempts) + expect(avg).toBeLessThan(5) // 5ms max per operation + }) + + it('add space', async () => { + const conn = await connect() + const account = await conn.getAccount() + const tx: TxCreateDoc = { + _class: core.class.TxCreateDoc, + space: core.space.Tx, + _id: generateId(), + objectSpace: core.space.Model, + modifiedBy: account._id, + modifiedOn: Date.now(), + attributes: { + name: 'Sp3', + description: '', + private: false, + archived: false, + members: [], + autoJoin: false + }, + objectClass: core.class.Space, + objectId: generateId() + } + await conn.tx(tx) + const spaces = await conn.findAll(core.class.Space, {}) + expect(spaces.length).toBe(3) + }) +}) diff --git a/server/ws/src/rpc.ts b/server/ws/src/rpc.ts new file mode 100644 index 0000000000..2d2685ff5f --- /dev/null +++ b/server/ws/src/rpc.ts @@ -0,0 +1,182 @@ +import type { Class, Doc, MeasureContext, Ref } from '@hcengineering/core' +import type { + ClientSessionCtx, + ConnectionSocket, + PipelineFactory, + Session, + SessionManager +} from '@hcengineering/server-core' +import { decodeToken } from '@hcengineering/server-token' + +import { type Express, type Response as ExpressResponse, type Request } from 'express' +import type { OutgoingHttpHeaders } from 'http2' +import { compress } from 'snappy' +import { promisify } from 'util' +import { gzip } from 'zlib' +import { retrieveJson } from './utils' +interface RPCClientInfo { + client: ConnectionSocket + session: Session + workspaceId: string +} + +const gzipAsync = promisify(gzip) + +const sendError = (res: ExpressResponse, code: number, data: any): void => { + res.writeHead(code, { + 'Content-Type': 'application/json', + 'Cache-Control': 'no-cache', + 'keep-alive': 'timeout=5, max=1000' + }) + res.end(JSON.stringify(data)) +} + +async function sendJson (req: Request, res: ExpressResponse, result: any): Promise { + const headers: OutgoingHttpHeaders = { + 'Content-Type': 'application/json', + 'Cache-Control': 'no-cache', + 'keep-alive': 'timeout=5, max=1000' + } + let body: any = JSON.stringify(result) + + const contentEncodings: string[] = + typeof req.headers['accept-encoding'] === 'string' + ? req.headers['accept-encoding'].split(',').map((it) => it.trim()) + : req.headers['accept-encoding'] ?? [] + for (const contentEncoding of contentEncodings) { + let done = false + switch (contentEncoding) { + case 'snappy': + headers['content-encoding'] = 'snappy' + body = await compress(body) + done = true + break + case 'gzip': + headers['content-encoding'] = 'gzip' + body = await gzipAsync(body) + done = true + break + } + if (done) { + break + } + } + + res.writeHead(200, headers) + res.end(body) +} + +export function registerRPC ( + app: Express, + sessions: SessionManager, + ctx: MeasureContext, + pipelineFactory: PipelineFactory +): void { + const rpcSessions = new Map() + + async function withSession ( + req: Request, + res: ExpressResponse, + operation: (ctx: ClientSessionCtx, session: Session) => Promise + ): Promise { + if (req.params.workspaceId === undefined || req.params.workspaceId === '') { + res.writeHead(400, {}) + res.end('Missing workspace') + return + } + let token = req.headers.authorization as string + if (token === null) { + sendError(res, 401, { message: 'Missing Authorization header' }) + return + } + const workspaceId = decodeURIComponent(req.params.workspaceId) + token = token.split(' ')[1] + + const decodedToken = decodeToken(token) + if (workspaceId !== decodedToken.workspace.name) { + sendError(res, 401, { message: 'Invalid workspace' }) + return + } + + let transactorRpc = rpcSessions.get(token) + + if (transactorRpc === undefined) { + const cs: ConnectionSocket = createClosingSocket(token, rpcSessions) + const s = await sessions.addSession(ctx, cs, decodedToken, token, pipelineFactory, token) + if (!('session' in s)) { + sendError(res, 401, { + message: 'Failed to create session', + mode: 'specialError' in s ? s.specialError ?? '' : 'upgrading' + }) + return + } + transactorRpc = { session: s.session, client: cs, workspaceId: s.workspaceId } + rpcSessions.set(token, transactorRpc) + } + try { + const rpc = transactorRpc + await sessions.handleRPC(ctx, rpc.session, rpc.client, async (ctx) => { + await operation(ctx, rpc.session) + }) + } catch (err: any) { + sendError(res, 401, { message: 'Failed to execute operation', error: err.message, stack: err.stack }) + } + } + + app.get('/api/v1/ping/:workspaceId', (req, res) => { + void withSession(req, res, async (ctx, session) => { + await session.ping(ctx) + await sendJson(req, res, { pong: true }) + }) + }) + + app.get('/api/v1/find-all/:workspaceId', (req, res) => { + void withSession(req, res, async (ctx, session) => { + const _class = req.query.class as Ref> + const query = req.query.query !== undefined ? JSON.parse(req.query.query as string) : {} + const options = req.query.options !== undefined ? JSON.parse(req.query.options as string) : {} + + const result = await session.findAllRaw(ctx.ctx, ctx.pipeline, _class, query, options) + await sendJson(req, res, result) + }) + }) + + app.post('/api/v1/find-all/:workspaceId', (req, res) => { + void withSession(req, res, async (ctx, session) => { + const { _class, query, options }: any = (await retrieveJson(req)) ?? {} + + const result = await session.findAllRaw(ctx.ctx, ctx.pipeline, _class, query, options) + await sendJson(req, res, result) + }) + }) + + app.post('/api/v1/tx/:workspaceId', (req, res) => { + void withSession(req, res, async (ctx, session) => { + const tx: any = (await retrieveJson(req)) ?? {} + + const result = await session.txRaw(ctx, tx) + await sendJson(req, res, result.result) + }) + }) + app.get('/api/v1/account/:workspaceId', (req, res) => { + void withSession(req, res, async (ctx, session) => { + const result = session.getRawAccount(ctx.pipeline) + await sendJson(req, res, result) + }) + }) +} + +function createClosingSocket (rawToken: string, rpcSessions: Map): ConnectionSocket { + return { + id: rawToken, + isClosed: false, + close: () => { + rpcSessions.delete(rawToken) + }, + send: (ctx, msg, binary, compression) => {}, + sendPong: () => {}, + data: () => ({}), + readRequest: (buffer, binary) => ({ method: '', params: [], id: -1, time: Date.now() }), + checkState: () => true + } +} diff --git a/server/ws/src/server_http.ts b/server/ws/src/server_http.ts index 873d061868..2cb116d099 100644 --- a/server/ws/src/server_http.ts +++ b/server/ws/src/server_http.ts @@ -59,6 +59,8 @@ import { WebSocketServer, type RawData, type WebSocket } from 'ws' import 'bufferutil' import { compress } from 'snappy' import 'utf-8-validate' +import { registerRPC } from './rpc' +import { retrieveJson } from './utils' let profiling = false const rpcHandler = new RPCHandler() @@ -151,6 +153,7 @@ export function startHttpServer ( res.end() } }) + app.get('/api/v1/profiling', (req, res) => { try { const token = req.query.token as string @@ -357,6 +360,8 @@ export function startHttpServer ( }) ) + registerRPC(app, sessions, ctx, pipelineFactory) + app.put('/api/v1/broadcast', (req, res) => { try { const token = req.query.token as string @@ -364,26 +369,19 @@ export function startHttpServer ( const ws = sessions.workspaces.get(req.query.workspace as WorkspaceUuid) if (ws !== undefined) { // push the data to body - const body: Buffer[] = [] - req - .on('data', (chunk) => { - body.push(chunk) - }) - .on('end', () => { - // on end of data, perform necessary action - try { - const data = JSON.parse(Buffer.concat(body as any).toString()) - if (Array.isArray(data)) { - sessions.broadcastAll(ws, data as Tx[]) - } else { - sessions.broadcastAll(ws, [data as unknown as Tx]) - } - res.end() - } catch (err: any) { - ctx.error('JSON parse error', { err }) - res.writeHead(400, {}) - res.end() + 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, {}) diff --git a/server/ws/src/utils.ts b/server/ws/src/utils.ts new file mode 100644 index 0000000000..ceae377428 --- /dev/null +++ b/server/ws/src/utils.ts @@ -0,0 +1,23 @@ +import type { Request } from 'express' + +export function retrieveJson (req: Request): Promise { + const body: Uint8Array[] = [] + return new Promise((resolve, reject) => { + req + .on('data', (chunk: Uint8Array) => { + body.push(chunk) + }) + .on('error', (err) => { + reject(err) + }) + .on('end', () => { + // on end of data, perform necessary action + try { + const data = JSON.parse(Buffer.concat(body).toString()) + resolve(data) + } catch (err: any) { + reject(err) + } + }) + }) +} From b917356d492a075d9410af1a02d0dd80f29e6f51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Henriques?= <50490202+andr3h3nriqu3s11@users.noreply.github.com> Date: Mon, 24 Feb 2025 06:11:57 +0000 Subject: [PATCH 4/7] Fix hideAdd does not prevent adding by keyboard on TagsPopup (#7246) Signed-off-by: Andre Henriques --- plugins/tags-resources/src/components/TagsPopup.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/tags-resources/src/components/TagsPopup.svelte b/plugins/tags-resources/src/components/TagsPopup.svelte index 7c7718f8db..517413896d 100644 --- a/plugins/tags-resources/src/components/TagsPopup.svelte +++ b/plugins/tags-resources/src/components/TagsPopup.svelte @@ -126,7 +126,7 @@ async function onSearchKeydown (ev: KeyboardEvent): Promise { if (ev.code !== 'Enter') return - if (!inProcess && objects.length < 1) { + if (!inProcess && !hideAdd && objects.length < 1) { inProcess = true await createTagElementQuick() ev.preventDefault() From 20bbe82144e41a92d555bd95fd7cec0ad41330a5 Mon Sep 17 00:00:00 2001 From: Alexander Onnikov Date: Mon, 24 Feb 2025 13:12:19 +0700 Subject: [PATCH 5/7] feat: cookie token (#8057) Signed-off-by: Alexander Onnikov --- .github/workflows/main.yml | 4 +- .vscode/launch.json | 38 +-- README.md | 6 +- common/config/rush/pnpm-lock.yaml | 10 +- desktop/.env-dev | 2 +- desktop/src/main/start.ts | 2 +- desktop/src/ui/index.ts | 24 +- dev/branding.json | 8 +- dev/docker-compose.yaml | 148 ++++----- dev/local-mongo/docker-compose.yaml | 18 +- dev/prod/public/branding.json | 8 +- dev/scripts/debug_account.sh | 4 +- dev/tool/src/index.ts | 29 +- packages/account-client/src/client.ts | 141 ++++++--- plugins/guest-resources/src/connect.ts | 45 +-- plugins/guest-resources/src/utils.ts | 17 +- plugins/login-resources/src/actions.ts | 2 - .../src/components/Auth.svelte | 11 +- .../src/components/Confirmation.svelte | 11 +- .../src/components/CreateWorkspaceForm.svelte | 11 +- .../src/components/Join.svelte | 3 +- .../src/components/LoginApp.svelte | 11 +- .../src/components/PasswordRestore.svelte | 9 +- .../src/components/SelectWorkspace.svelte | 17 +- .../src/components/SignupForm.svelte | 7 +- plugins/login-resources/src/utils.ts | 84 ++--- plugins/login/src/index.ts | 3 +- .../src/components/Auth.svelte | 15 +- .../src/components/OnboardApp.svelte | 14 +- .../src/components/Profile.svelte | 19 +- .../src/components/Settings.svelte | 28 +- .../src/components/AccountPopup.svelte | 7 +- .../ServerManagerServerStatistics.svelte | 1 - .../src/components/ServerManagerUsers.svelte | 3 +- .../src/components/Workbench.svelte | 8 +- plugins/workbench-resources/src/connect.ts | 56 +--- plugins/workbench-resources/src/index.ts | 7 +- plugins/workbench-resources/src/utils.ts | 38 ++- plugins/workbench/src/index.ts | 296 +----------------- plugins/workbench/src/plugin.ts | 108 +++++++ plugins/workbench/src/types.ts | 213 +++++++++++++ plugins/workbench/src/utils.ts | 29 ++ pods/fulltext/run.sh | 2 +- pods/green/src/index.ts | 2 +- pods/server/package.json | 4 +- qms-tests/branding-test.json | 2 +- qms-tests/docker-compose.yaml | 40 +-- qms-tests/sanity/.env | 4 +- qms-tests/sanity/package.json | 10 +- server/account-service/package.json | 2 + server/account-service/src/index.ts | 95 +++++- server/account/src/__tests__/utils.test.ts | 49 +-- server/account/src/operations.ts | 235 +++++++++----- server/account/src/types.ts | 2 +- server/account/src/utils.ts | 18 +- services/sign/pod-sign/debug/branding.json | 8 +- tests/create-local.sh | 4 +- tests/sanity/tests/API/Api.ts | 14 +- .../sanity/tests/model/left-side-menu-page.ts | 27 +- tests/sanity/tests/workspace/create.spec.ts | 33 +- workers/transactor/wrangler.toml | 2 +- ws-tests/create-local.sh | 4 +- .../sanity/tests/workspace/archive.spec.ts | 3 + .../sanity/tests/workspace/migrate.spec.ts | 5 + ws-tests/tool-local.sh | 14 + 65 files changed, 1136 insertions(+), 958 deletions(-) create mode 100644 plugins/workbench/src/plugin.ts create mode 100644 plugins/workbench/src/types.ts create mode 100644 plugins/workbench/src/utils.ts create mode 100755 ws-tests/tool-local.sh diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0ce40f73f5..dbbad43dcc 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -409,7 +409,7 @@ jobs: DOCKER_CLI_HINTS: false - name: Configure /etc/hosts run: | - sudo echo "127.0.0.1 host.docker.internal" | sudo tee -a /etc/hosts + sudo echo "127.0.0.1 huly.local" | sudo tee -a /etc/hosts - name: Prepare server run: | cd ./qms-tests @@ -479,7 +479,7 @@ jobs: DOCKER_CLI_HINTS: false - name: Configure /etc/hosts run: | - sudo echo "127.0.0.1 host.docker.internal" | sudo tee -a /etc/hosts + sudo echo "127.0.0.1 huly.local" | sudo tee -a /etc/hosts - name: Prepare server run: | cd ./ws-tests diff --git a/.vscode/launch.json b/.vscode/launch.json index d3ee324774..d78c98e888 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -35,12 +35,12 @@ "args": ["src/__start.ts"], "env": { // "FULLTEXT_URL": "http://localhost:4700", - "FULLTEXT_URL": "http://host.docker.internal:4702", + "FULLTEXT_URL": "http://huly.local:4702", // "MONGO_URL": "mongodb://localhost:27017", // "DB_URL": "mongodb://localhost:27017", // "DB_URL": "postgresql://postgres:example@localhost:5432", - "DB_URL": "postgresql://root@host.docker.internal:26257/defaultdb?sslmode=disable", - "GREEN_URL": "http://host.docker.internal:6767?token=secret", + "DB_URL": "postgresql://root@huly.local:26257/defaultdb?sslmode=disable", + "GREEN_URL": "http://huly.local:6767?token=secret", "SERVER_PORT": "3332", "APM_SERVER_URL2": "http://localhost:8200", "METRICS_CONSOLE": "false", @@ -60,7 +60,7 @@ "ELASTIC_INDEX_NAME": "local_storage_index", "UPLOAD_URL": "/files", "AI_BOT_URL": "http://localhost:4010", - "STATS_URL": "http://host.docker.internal:4900" + "STATS_URL": "http://huly.local:4900" }, "runtimeArgs": ["--nolazy", "-r", "ts-node/register"], "runtimeVersion": "20", @@ -79,8 +79,8 @@ "FULLTEXT_URL": "http://localhost:4710", // "DB_URL": "mongodb://localhost:27018", // "DB_URL": "postgresql://postgres:example@localhost:5432", - "DB_URL": "postgresql://root@host.docker.internal:26258/defaultdb?sslmode=disable", - // "GREEN_URL": "http://host.docker.internal:6767?token=secret", + "DB_URL": "postgresql://root@huly.local:26258/defaultdb?sslmode=disable", + // "GREEN_URL": "http://huly.local:6767?token=secret", "SERVER_PORT": "3335", "METRICS_CONSOLE": "false", "METRICS_FILE": "${workspaceRoot}/metrics.txt", // Show metrics in console evert 30 seconds., @@ -92,7 +92,7 @@ "ACCOUNTS_URL": "http://localhost:3003", "MODEL_JSON": "${workspaceRoot}/models/all/bundle/model.json", "MODEL_VERSION": "0.7.1", - "STATS_URL": "http://host.docker.internal:4901" + "STATS_URL": "http://huly.local:4901" }, "runtimeArgs": ["--nolazy", "-r", "ts-node/register"], "runtimeVersion": "20", @@ -113,13 +113,13 @@ "FULLTEXT_DB_URL": "http://localhost:9200", // "DB_URL": "mongodb://localhost:27017", // "DB_URL": "postgresql://postgres:example@localhost:5432", - "DB_URL": "postgresql://root@host.docker.internal:26257/defaultdb?sslmode=disable", + "DB_URL": "postgresql://root@huly.local:26257/defaultdb?sslmode=disable", "STORAGE_CONFIG": "minio|localhost?accessKey=minioadmin&secretKey=minioadmin", "SERVER_SECRET": "secret", "REKONI_URL": "http://localhost:4004", "MODEL_JSON": "${workspaceRoot}/models/all/bundle/model.json", "ELASTIC_INDEX_NAME": "local_storage_index", - "STATS_URL":"http://host.docker.internal:4900", + "STATS_URL":"http://huly.local:4900", "ACCOUNTS_URL": "http://localhost:3000", }, "runtimeArgs": ["--nolazy", "-r", "ts-node/register"], @@ -138,14 +138,14 @@ "env": { "MONGO_URL": "mongodb://localhost:27017", "DB_URL": "mongodb://localhost:27017", - // "DB_URL": "postgresql://root@host.docker.internal:26257/defaultdb?sslmode=disable", + // "DB_URL": "postgresql://root@huly.local:26257/defaultdb?sslmode=disable", "SERVER_SECRET": "secret", "REGION_INFO":"|Mongo;cockroach|CockroachDB", - "TRANSACTOR_URL": "ws://host.docker.internal:3333,ws://host.docker.internal:3332;;cockroach", + "TRANSACTOR_URL": "ws://huly.local:3333,ws://huly.local:3332;;cockroach", "ACCOUNTS_URL": "http://localhost:3000", "ACCOUNT_PORT": "3000", "FRONT_URL": "http://localhost:8080", - "STATS_URL": "http://host.docker.internal:4900", + "STATS_URL": "http://huly.local:4900", "SES_URL": "", // "DB_NS": "account-2", // "WS_LIVENESS_DAYS": "1", @@ -172,11 +172,11 @@ // "DB_URL": "postgresql://postgres:example@localhost:5432", "SERVER_SECRET": "secret", "REGION_INFO":"|Mongo;pg|Postgres;cockroach|CockroachDB", - "TRANSACTOR_URL": "ws://host.docker.internal:3334;;,ws://host.docker.internal:3335;;europe", + "TRANSACTOR_URL": "ws://huly.local:3334;;,ws://huly.local:3335;;europe", "ACCOUNTS_URL": "http://localhost:3003", "ACCOUNT_PORT": "3003", "FRONT_URL": "http://localhost:8083", - "STATS_URL": "http://host.docker.internal:4901", + "STATS_URL": "http://huly.local:4901", "SES_URL": "", // "DB_NS": "account-2", // "WS_LIVENESS_DAYS": "1", @@ -216,7 +216,7 @@ "env": { "DB_URL": "mongodb://localhost:27017", // "DB_URL": "postgresql://postgres:example@localhost:5432", - // "DB_URL": "postgresql://root@host.docker.internal:26257/defaultdb?sslmode=disable", + // "DB_URL": "postgresql://root@huly.local:26257/defaultdb?sslmode=disable", "REGION": "", "SERVER_SECRET": "secret", @@ -249,8 +249,8 @@ "env": { // "DB_URL": "mongodb://localhost:27017", // "DB_URL": "postgresql://postgres:example@localhost:5432", - "DB_URL": "postgresql://root@host.docker.internal:26257/defaultdb?sslmode=disable", - "FULLTEXT_URL": "http://host.docker.internal:4702", + "DB_URL": "postgresql://root@huly.local:26257/defaultdb?sslmode=disable", + "FULLTEXT_URL": "http://huly.local:4702", "REGION": "cockroach", "SERVER_SECRET": "secret", "TRANSACTOR_URL": "ws://localhost:3332", @@ -415,7 +415,7 @@ "ACCOUNT_DB_URL": "mongodb://localhost:27017", // "ACCOUNT_DB_URL": "postgresql://postgres:example@localhost:5433", // "DB_URL": "postgresql://postgres:example@localhost:5433", - "DB_URL": "postgresql://root@host.docker.internal:26257/defaultdb?sslmode=disable", + "DB_URL": "postgresql://root@huly.local:26257/defaultdb?sslmode=disable", "MONGO_URL": "mongodb://localhost:27017", "TELEGRAM_DATABASE": "telegram-service", "REKONI_URL": "http://localhost:4004", @@ -514,7 +514,7 @@ "PLATFORM_OPERATION_LOGGING": "true", "FRONT_URL": "http://localhost:8080", "PORT": "3500", - "STATS_URL": "http://host.docker.internal:4900" + "STATS_URL": "http://huly.local:4900" }, "runtimeArgs": ["--nolazy", "-r", "ts-node/register"], "sourceMaps": true, diff --git a/README.md b/README.md index 3f50824cf1..6335127895 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,8 @@ This project offers a convenient method to host Huly using `docker`, designed fo - [Table of Content](#table-of-content) - [Pre-requisites](#pre-requisites) - [Verification](#verification) + - [Fast start](#fast-start) + - [Branches \& Contributing](#branches--contributing) - [Installation](#installation) - [Build and run](#build-and-run) - [Run in development mode](#run-in-development-mode) @@ -149,10 +151,10 @@ sh ./scripts/create-workspace.sh Add the following line to your /etc/hosts file ```plain -127.0.0.1 host.docker.internal +127.0.0.1 huly.local ``` -Accessing the URL will lead you to the app in development mode. +Accessing the URL will lead you to the app in development mode. Limitations: diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index b4c2c57fbf..48d4de2acd 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -1330,6 +1330,9 @@ importers: '@types/compression': specifier: ~1.7.2 version: 1.7.5 + '@types/cookies': + specifier: ^0.9.0 + version: 0.9.0 '@types/cors': specifier: ^2.8.12 version: 2.8.17 @@ -1534,6 +1537,9 @@ importers: compression-webpack-plugin: specifier: ^10.0.0 version: 10.0.0(webpack@5.97.1) + cookies: + specifier: ^0.9.1 + version: 0.9.1 copy-webpack-plugin: specifier: ^11.0.0 version: 11.0.0(webpack@5.97.1) @@ -3955,7 +3961,7 @@ packages: version: 0.0.0 '@rush-temp/account-service@file:projects/account-service.tgz': - resolution: {integrity: sha512-SBWP6HBOnzpJC3MIOlYbttcjYw1HoD9VpbLpUS3zRwEL2slU6c0W/hEBU0/rzh3KkLb6bScKssS5utlSx5RrNw==, tarball: file:projects/account-service.tgz} + resolution: {integrity: sha512-ViPfMFYJ6+K9NPAIyV9v6+v6BRtdPCSiU4iTXCdZsoPgpag3nFxhE5PC3ahzS2wJ0TbCX+5UdaUX/UE3ySL4OA==, tarball: file:projects/account-service.tgz} version: 0.0.0 '@rush-temp/account@file:projects/account.tgz': @@ -16067,6 +16073,7 @@ snapshots: '@rush-temp/account-service@file:projects/account-service.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3)': dependencies: '@koa/cors': 5.0.0 + '@types/cookies': 0.9.0 '@types/jest': 29.5.12 '@types/koa': 2.15.0 '@types/koa-bodyparser': 4.3.12 @@ -16075,6 +16082,7 @@ snapshots: '@types/node': 20.11.19 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.3.3))(eslint@8.56.0)(typescript@5.3.3) '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3) + cookies: 0.9.1 cross-env: 7.0.3 esbuild: 0.24.2 eslint: 8.56.0 diff --git a/desktop/.env-dev b/desktop/.env-dev index 037febc783..936dd726fd 100644 --- a/desktop/.env-dev +++ b/desktop/.env-dev @@ -1,2 +1,2 @@ -FRONT_URL=http://localhost:8087 +FRONT_URL=http://huly.local:8087 CONFIG_URL=config.json diff --git a/desktop/src/main/start.ts b/desktop/src/main/start.ts index fee9be59bc..6c276e1aa6 100644 --- a/desktop/src/main/start.ts +++ b/desktop/src/main/start.ts @@ -63,7 +63,7 @@ const serverChanged = oldFront !== FRONT_URL function readServerUrl (): string { if (isDev) { - return process.env.FRONT_URL ?? 'http://localhost:8087' + return process.env.FRONT_URL ?? 'http://huly.local:8087' } return ((settings as any).get('server', process.env.FRONT_URL) as string) ?? 'https://huly.app' diff --git a/desktop/src/ui/index.ts b/desktop/src/ui/index.ts index 2658d01ea9..499a86d5f5 100644 --- a/desktop/src/ui/index.ts +++ b/desktop/src/ui/index.ts @@ -1,24 +1,21 @@ -import login, { loginId } from '@hcengineering/login' -import { getEmbeddedLabel, getMetadata, setMetadata } from '@hcengineering/platform' -import presentation, { closeClient, MessageBox, setDownloadProgress } from '@hcengineering/presentation' +import { loginId } from '@hcengineering/login' +import { getEmbeddedLabel, getMetadata } from '@hcengineering/platform' +import presentation, { MessageBox, setDownloadProgress } from '@hcengineering/presentation' import settings, { settingId } from '@hcengineering/setting' import { closePanel, closePopup, createApp, - fetchMetadataLocalStorage, - getCurrentLocation, getCurrentResolvedLocation, navigate, parseLocation, pushRootBarProgressComponent, removeRootBarComponent, - setMetadataLocalStorage, showPopup } from '@hcengineering/ui' import { notificationId } from '@hcengineering/notification' -import { workbenchId } from '@hcengineering/workbench' +import { workbenchId, logOut } from '@hcengineering/workbench' import { isOwnerOrMaintainer } from '@hcengineering/core' import { configurePlatform } from './platform' @@ -58,18 +55,7 @@ window.addEventListener('DOMContentLoaded', () => { }) ipcMain.on('logout', () => { - const tokens = fetchMetadataLocalStorage(login.metadata.LoginTokensV2) - if (tokens !== null) { - const loc = getCurrentLocation() - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete tokens[loc.path[1]] - setMetadataLocalStorage(login.metadata.LoginTokensV2, tokens) - } - setMetadata(presentation.metadata.Token, null) - setMetadataLocalStorage(login.metadata.LastToken, null) - setMetadataLocalStorage(login.metadata.LoginEndpoint, null) - setMetadataLocalStorage(login.metadata.LoginAccount, null) - void closeClient().then(() => { + void logOut().then(() => { navigate({ path: [loginId] }) }) }) diff --git a/dev/branding.json b/dev/branding.json index b35a6042a5..44b2d6d91e 100644 --- a/dev/branding.json +++ b/dev/branding.json @@ -1,12 +1,12 @@ { - "localhost:8080": { + "huly.local:8080": { "key": "huly-dev", "title": "Huly", "protocol": "http", "language": "en", "lastNameFirst": "true" }, - "localhost:8087": { + "huly.local:8087": { "key": "huly", "title": "Huly", "protocol": "http", @@ -20,13 +20,13 @@ "language": "en", "lastNameFirst": "true" }, - "localhost:8081": { + "huly.local:8081": { "key": "tracex-dev", "title": "TraceX", "protocol": "http", "language": "en" }, - "localhost:8088": { + "huly.local:8088": { "key": "tracex", "title": "TraceX", "protocol": "http", diff --git a/dev/docker-compose.yaml b/dev/docker-compose.yaml index 99a9ceab8e..99ca8cf30a 100644 --- a/dev/docker-compose.yaml +++ b/dev/docker-compose.yaml @@ -3,7 +3,7 @@ services: image: 'mongo:7-jammy' container_name: mongodb extra_hosts: - - 'host.docker.internal:host-gateway' + - 'huly.local:host-gateway' healthcheck: test: echo "try { db.currentOp().ok } catch (err) { }" | mongosh --port 27017 --quiet interval: 5s @@ -61,7 +61,7 @@ services: account: image: hardcoreeng/account extra_hosts: - - 'host.docker.internal:host-gateway' + - 'huly.local:host-gateway' links: - mongodb - minio @@ -74,29 +74,29 @@ services: - ACCOUNT_PORT=3000 - SERVER_SECRET=secret - ADMIN_EMAILS=admin - - STATS_URL=http://host.docker.internal:4900 - # - DB_URL=postgresql://root@host.docker.internal:26257/defaultdb?sslmode=disable + - STATS_URL=http://huly.local:4900 + # - DB_URL=postgresql://root@huly.local:26257/defaultdb?sslmode=disable - DB_URL=${MONGO_URL} # - DB_NS=account-2 # Pass only one region to disallow selection for new workspaces.Ø - REGION_INFO=|Mongo;cockroach|CockroachDB # - REGION_INFO=cockroach|CockroachDB - - TRANSACTOR_URL=ws://host.docker.internal:3333,ws://host.docker.internal:3332;;cockroach, + - TRANSACTOR_URL=ws://huly.local:3333,ws://huly.local:3332;;cockroach, - SES_URL= - STORAGE_CONFIG=${STORAGE_CONFIG} - - FRONT_URL=http://host.docker.internal:8087 + - FRONT_URL=http://huly.local:8087 - RESERVED_DB_NAMES=telegram,gmail,github - MODEL_ENABLED=* - LAST_NAME_FIRST=true # - WS_LIVENESS_DAYS=1 - - ACCOUNTS_URL=http://host.docker.internal:3000 + - ACCOUNTS_URL=http://huly.local:3000 - BRANDING_PATH=/var/cfg/branding.json # - DISABLE_SIGNUP=true restart: unless-stopped stats: image: hardcoreeng/stats extra_hosts: - - 'host.docker.internal:host-gateway' + - 'huly.local:host-gateway' ports: - 4900:4900 environment: @@ -106,7 +106,7 @@ services: workspace: image: hardcoreeng/workspace extra_hosts: - - 'host.docker.internal:host-gateway' + - 'huly.local:host-gateway' links: - mongodb - minio @@ -117,12 +117,12 @@ services: - WS_OPERATION=all+backup - SERVER_SECRET=secret - DB_URL=${MONGO_URL} - - STATS_URL=http://host.docker.internal:4900 + - STATS_URL=http://huly.local:4900 - SES_URL= - STORAGE_CONFIG=${STORAGE_CONFIG} - RESERVED_DB_NAMES=telegram,gmail,github - MODEL_ENABLED=* - - ACCOUNTS_URL=http://host.docker.internal:3000 + - ACCOUNTS_URL=http://huly.local:3000 - BRANDING_PATH=/var/cfg/branding.json # - PARALLEL=2 - BACKUP_STORAGE=${BACKUP_STORAGE_CONFIG} @@ -132,7 +132,7 @@ services: workspace_cockroach: image: hardcoreeng/workspace extra_hosts: - - 'host.docker.internal:host-gateway' + - 'huly.local:host-gateway' links: - cockroach - minio @@ -142,14 +142,14 @@ services: environment: - WS_OPERATION=all+backup - SERVER_SECRET=secret - - DB_URL=postgresql://root@host.docker.internal:26257/defaultdb?sslmode=disable - - STATS_URL=http://host.docker.internal:4900 + - DB_URL=postgresql://root@huly.local:26257/defaultdb?sslmode=disable + - STATS_URL=http://huly.local:4900 - SES_URL= - REGION=cockroach - STORAGE_CONFIG=${STORAGE_CONFIG} - RESERVED_DB_NAMES=telegram,gmail,github - MODEL_ENABLED=* - - ACCOUNTS_URL=http://host.docker.internal:3000 + - ACCOUNTS_URL=http://huly.local:3000 - BRANDING_PATH=/var/cfg/branding.json # - PARALLEL=2 - BACKUP_STORAGE=${BACKUP_STORAGE_CONFIG} @@ -159,7 +159,7 @@ services: collaborator: image: hardcoreeng/collaborator extra_hosts: - - 'host.docker.internal:host-gateway' + - 'huly.local:host-gateway' links: - mongodb - minio @@ -170,14 +170,14 @@ services: environment: - COLLABORATOR_PORT=3078 - SECRET=secret - - ACCOUNTS_URL=http://host.docker.internal:3000 + - ACCOUNTS_URL=http://huly.local:3000 - STORAGE_CONFIG=${STORAGE_CONFIG} - - STATS_URL=http://host.docker.internal:4900 + - STATS_URL=http://huly.local:4900 restart: unless-stopped front: image: hardcoreeng/front extra_hosts: - - 'host.docker.internal:host-gateway' + - 'huly.local:host-gateway' links: - mongodb - minio @@ -191,28 +191,28 @@ services: environment: - SERVER_PORT=8080 - SERVER_SECRET=secret - - ACCOUNTS_URL=http://host.docker.internal:3000 - - STATS_URL=http://host.docker.internal:4900 + - ACCOUNTS_URL=http://huly.local:3000 + - STATS_URL=http://huly.local:4900 - UPLOAD_URL=/files - - GMAIL_URL=http://host.docker.internal:8088 - - CALENDAR_URL=http://host.docker.internal:8095 - - TELEGRAM_URL=http://host.docker.internal:8086 - - REKONI_URL=http://host.docker.internal:4004 - - COLLABORATOR_URL=ws://host.docker.internal:3078 + - GMAIL_URL=http://huly.local:8088 + - CALENDAR_URL=http://huly.local:8095 + - TELEGRAM_URL=http://huly.local:8086 + - REKONI_URL=http://huly.local:4004 + - COLLABORATOR_URL=ws://huly.local:3078 - STORAGE_CONFIG=${STORAGE_CONFIG} - - GITHUB_URL=http://host.docker.internal:3500 - - PRINT_URL=http://host.docker.internal:4005 - - SIGN_URL=http://host.docker.internal:4006 - - ANALYTICS_COLLECTOR_URL=http://host.docker.internal:4017 + - GITHUB_URL=http://huly.local:3500 + - PRINT_URL=http://huly.local:4005 + - SIGN_URL=http://huly.local:4006 + - ANALYTICS_COLLECTOR_URL=http://huly.local:4017 - DESKTOP_UPDATES_URL=https://dist.huly.io - DESKTOP_UPDATES_CHANNEL=dev - - BRANDING_URL=http://host.docker.internal:8087/branding.json + - BRANDING_URL=http://huly.local:8087/branding.json # - DISABLE_SIGNUP=true restart: unless-stopped transactor: image: hardcoreeng/transactor extra_hosts: - - 'host.docker.internal:host-gateway' + - 'huly.local:host-gateway' links: - mongodb - minio @@ -230,8 +230,8 @@ services: - SERVER_PORT=3333 - SERVER_SECRET=secret - ENABLE_COMPRESSION=false - - STATS_URL=http://host.docker.internal:4900 - - FULLTEXT_URL=http://host.docker.internal:4700 + - STATS_URL=http://huly.local:4900 + - FULLTEXT_URL=http://huly.local:4700 # - DB_URL=postgresql://postgres:example@postgres:5432 - DB_URL=${MONGO_URL} - MONGO_URL=${MONGO_URL} @@ -239,18 +239,18 @@ services: - METRICS_CONSOLE=false - METRICS_FILE=metrics.txt - STORAGE_CONFIG=${STORAGE_CONFIG} - - FRONT_URL=http://host.docker.internal:8087 + - FRONT_URL=http://huly.local:8087 # - APM_SERVER_URL=http://apm-server:8200 - SES_URL='' - - ACCOUNTS_URL=http://host.docker.internal:3000 + - ACCOUNTS_URL=http://huly.local:3000 - LAST_NAME_FIRST=true - BRANDING_PATH=/var/cfg/branding.json - - AI_BOT_URL=http://host.docker.internal:4010 + - AI_BOT_URL=http://huly.local:4010 restart: unless-stopped transactor_cockroach: image: hardcoreeng/transactor extra_hosts: - - 'host.docker.internal:host-gateway' + - 'huly.local:host-gateway' links: - cockroach - minio @@ -268,24 +268,24 @@ services: - SERVER_PORT=3332 - SERVER_SECRET=secret - ENABLE_COMPRESSION=false - - FULLTEXT_URL=http://host.docker.internal:4702 - - STATS_URL=http://host.docker.internal:4900 - - DB_URL=postgresql://root@host.docker.internal:26257/defaultdb?sslmode=disable + - FULLTEXT_URL=http://huly.local:4702 + - STATS_URL=http://huly.local:4900 + - DB_URL=postgresql://root@huly.local:26257/defaultdb?sslmode=disable - METRICS_CONSOLE=false - METRICS_FILE=metrics.txt - STORAGE_CONFIG=${STORAGE_CONFIG} - - FRONT_URL=http://host.docker.internal:8087 + - FRONT_URL=http://huly.local:8087 # - APM_SERVER_URL=http://apm-server:8200 - SES_URL='' - - ACCOUNTS_URL=http://host.docker.internal:3000 + - ACCOUNTS_URL=http://huly.local:3000 - LAST_NAME_FIRST=true - BRANDING_PATH=/var/cfg/branding.json - - AI_BOT_URL=http://host.docker.internal:4010 + - AI_BOT_URL=http://huly.local:4010 restart: unless-stopped green: image: hardcoreeng/green extra_hosts: - - 'host.docker.internal:host-gateway' + - 'huly.local:host-gateway' links: - cockroach - stats @@ -294,8 +294,8 @@ services: environment: - PORT=6767 - AUTH_TOKEN=secret - - STATS_URL=http://host.docker.internal:4900 - - DB_URL=postgresql://root@host.docker.internal:26257/defaultdb?sslmode=disable + - STATS_URL=http://huly.local:4900 + - DB_URL=postgresql://root@huly.local:26257/defaultdb?sslmode=disable restart: unless-stopped rekoni: image: hardcoreeng/rekoni-service @@ -305,7 +305,7 @@ services: fulltext: image: hardcoreeng/fulltext extra_hosts: - - 'host.docker.internal:host-gateway' + - 'huly.local:host-gateway' restart: unless-stopped links: - elastic @@ -315,16 +315,16 @@ services: environment: - SERVER_SECRET=secret - DB_URL=${MONGO_URL} - - FULLTEXT_DB_URL=http://host.docker.internal:9200 + - FULLTEXT_DB_URL=http://huly.local:9200 - ELASTIC_INDEX_NAME=local_storage_index - STORAGE_CONFIG=${STORAGE_CONFIG} - - STATS_URL=http://host.docker.internal:4900 - - REKONI_URL=http://host.docker.internal:4004 - - ACCOUNTS_URL=http://host.docker.internal:3000 + - STATS_URL=http://huly.local:4900 + - REKONI_URL=http://huly.local:4004 + - ACCOUNTS_URL=http://huly.local:3000 fulltext_cockroach: image: hardcoreeng/fulltext extra_hosts: - - 'host.docker.internal:host-gateway' + - 'huly.local:host-gateway' restart: unless-stopped links: - elastic @@ -334,29 +334,29 @@ services: environment: - PORT=4702 - SERVER_SECRET=secret - - DB_URL=postgresql://root@host.docker.internal:26257/defaultdb?sslmode=disable - - FULLTEXT_DB_URL=http://host.docker.internal:9200 + - DB_URL=postgresql://root@huly.local:26257/defaultdb?sslmode=disable + - FULLTEXT_DB_URL=http://huly.local:9200 - ELASTIC_INDEX_NAME=local_storage_index - STORAGE_CONFIG=${STORAGE_CONFIG} - - STATS_URL=http://host.docker.internal:4900 - - REKONI_URL=http://host.docker.internal:4004 - - ACCOUNTS_URL=http://host.docker.internal:3000 + - STATS_URL=http://huly.local:4900 + - REKONI_URL=http://huly.local:4004 + - ACCOUNTS_URL=http://huly.local:3000 print: image: hardcoreeng/print extra_hosts: - - 'host.docker.internal:host-gateway' + - 'huly.local:host-gateway' restart: unless-stopped ports: - 4005:4005 environment: - SECRET=secret - STORAGE_CONFIG=${STORAGE_CONFIG} - - STATS_URL=http://host.docker.internal:4900 - - ACCOUNTS_URL=http://host.docker.internal:3000 + - STATS_URL=http://huly.local:4900 + - ACCOUNTS_URL=http://huly.local:3000 sign: image: hardcoreeng/sign extra_hosts: - - 'host.docker.internal:host-gateway' + - 'huly.local:host-gateway' restart: unless-stopped ports: - 4006:4006 @@ -367,16 +367,16 @@ services: - SECRET=secret - MINIO_ENDPOINT=minio - MINIO_ACCESS_KEY=minioadmin - - ACCOUNTS_URL=http://host.docker.internal:3000 + - ACCOUNTS_URL=http://huly.local:3000 - MINIO_SECRET_KEY=minioadmin - CERTIFICATE_PATH=/var/cfg/certificate.p12 - SERVICE_ID=sign-service - BRANDING_PATH=/var/cfg/branding.json - - STATS_URL=http://host.docker.internal:4900 + - STATS_URL=http://huly.local:4900 # analytics: # image: hardcoreeng/analytics-collector # extra_hosts: -# - 'host.docker.internal:host-gateway' +# - 'huly.local:host-gateway' # restart: unless-stopped # ports: # - 4017:4017 @@ -386,32 +386,32 @@ services: # - MONGO_URL=${MONGO_URL} # - 'MONGO_OPTIONS={"appName":"analytics","maxPoolSize":1}' # - SERVICE_ID=analytics-collector-service -# - ACCOUNTS_URL=http://host.docker.internal:3000 -# - STATS_URL=http://host.docker.internal:4900 +# - ACCOUNTS_URL=http://huly.local:3000 +# - STATS_URL=http://huly.local:4900 aiBot: image: hardcoreeng/ai-bot ports: - 4010:4010 extra_hosts: - - 'host.docker.internal:host-gateway' + - 'huly.local:host-gateway' restart: unless-stopped environment: - SERVER_SECRET=secret - MONGO_URL=${MONGO_URL} - - ACCOUNTS_URL=http://host.docker.internal:3000 + - ACCOUNTS_URL=http://huly.local:3000 - STORAGE_CONFIG=${STORAGE_CONFIG} - FIRST_NAME=Jolie - LAST_NAME=AI - PASSWORD=password - AVATAR_PATH=./avatar.png - AVATAR_CONTENT_TYPE=.png - - STATS_URL=http://host.docker.internal:4900 -# - LOVE_ENDPOINT=http://host.docker.internal:8096 + - STATS_URL=http://huly.local:4900 +# - LOVE_ENDPOINT=http://huly.local:8096 # - OPENAI_API_KEY=token # telegram-bot: # image: hardcoreeng/telegram-bot # extra_hosts: -# - "host.docker.internal:host-gateway" +# - "huly.local:host-gateway" # restart: unless-stopped # environment: # - PORT=4020 @@ -420,9 +420,9 @@ services: # - MONGO_DB=telegram-bot # - SECRET=secret # - DOMAIN=domain -# - ACCOUNTS_URL=http://host.docker.internal:3000 +# - ACCOUNTS_URL=http://huly.local:3000 # - SERVICE_ID=telegram-bot-service -# - STATS_URL=http://host.docker.internal:4900 +# - STATS_URL=http://huly.local:4900 volumes: db: dbpg: diff --git a/dev/local-mongo/docker-compose.yaml b/dev/local-mongo/docker-compose.yaml index 22b38fdf09..5576d357c2 100644 --- a/dev/local-mongo/docker-compose.yaml +++ b/dev/local-mongo/docker-compose.yaml @@ -40,7 +40,7 @@ services: environment: - ACCOUNT_PORT=3000 - SERVER_SECRET=secret - - MONGO_URL=mongodb://host.docker.internal:27017?compressors=snappy + - MONGO_URL=mongodb://huly.local:27017?compressors=snappy - TRANSACTOR_URL=ws://transactor:3333;ws://localhost:3333 - SES_URL= - STORAGE_CONFIG=${STORAGE_CONFIG} @@ -65,7 +65,7 @@ services: - SECRET=secret - ACCOUNTS_URL=http://account:3000 - UPLOAD_URL=/files - - MONGO_URL=mongodb://host.docker.internal:27017?compressors=snappy + - MONGO_URL=mongodb://huly.local:27017?compressors=snappy - 'MONGO_OPTIONS={"appName":"collaborator","maxPoolSize":2}' - STORAGE_CONFIG=${STORAGE_CONFIG} restart: unless-stopped @@ -83,7 +83,7 @@ services: - UV_THREADPOOL_SIZE=10 - SERVER_PORT=8080 - SERVER_SECRET=secret - - MONGO_URL=mongodb://host.docker.internal:27017?compressors=snappy + - MONGO_URL=mongodb://huly.local:27017?compressors=snappy - 'MONGO_OPTIONS={"appName":"front","maxPoolSize":1}' - ACCOUNTS_URL=http://localhost:3000 - UPLOAD_URL=/files @@ -120,7 +120,7 @@ services: - SERVER_PORT=3333 - SERVER_SECRET=secret - ENABLE_COMPRESSION=true - - MONGO_URL=mongodb://host.docker.internal:27017?compressors=snappy + - MONGO_URL=mongodb://huly.local:27017?compressors=snappy - 'MONGO_OPTIONS={"appName": "transactor", "maxPoolSize": 10}' - METRICS_CONSOLE=false - METRICS_FILE=metrics.txt @@ -151,7 +151,7 @@ services: - 4005:4005 environment: - SECRET=secret - - MONGO_URL=mongodb://host.docker.internal:27017?compressors=snappy + - MONGO_URL=mongodb://huly.local:27017?compressors=snappy - 'MONGO_OPTIONS={"appName":"print","maxPoolSize":1}' - STORAGE_CONFIG=${STORAGE_CONFIG} deploy: @@ -168,7 +168,7 @@ services: - ../../services/sign/pod-sign/debug/branding.json:/var/cfg/branding.json environment: - SECRET=secret - - MONGO_URL=mongodb://host.docker.internal:27017 + - MONGO_URL=mongodb://huly.local:27017 - 'MONGO_OPTIONS={"appName":"sign","maxPoolSize":1}' - MINIO_ENDPOINT=minio - MINIO_ACCESS_KEY=minioadmin @@ -189,7 +189,7 @@ services: # environment: # - SECRET=secret # - PORT=4007 -# - MONGO_URL=mongodb://host.docker.internal:27017 +# - MONGO_URL=mongodb://huly.local:27017 # - 'MONGO_OPTIONS={"appName":"analytics","maxPoolSize":1}' # - SERVICE_ID=analytics-collector-service # - ACCOUNTS_URL=http://account:3000 @@ -202,7 +202,7 @@ services: restart: unless-stopped environment: - SERVER_SECRET=secret - - MONGO_URL=mongodb://host.docker.internal:27017 + - MONGO_URL=mongodb://huly.local:27017 - ACCOUNTS_URL=http://account:3000 - FIRST_NAME=Jolie - LAST_NAME=AI @@ -219,7 +219,7 @@ services: # environment: # - PORT=4020 # - BOT_TOKEN=token -# - MONGO_URL=mongodb://host.docker.internal:27017 +# - MONGO_URL=mongodb://huly.local:27017 # - MONGO_DB=telegram-bot # - SECRET=secret # - DOMAIN=domain diff --git a/dev/prod/public/branding.json b/dev/prod/public/branding.json index 1efd403c58..caccda656f 100644 --- a/dev/prod/public/branding.json +++ b/dev/prod/public/branding.json @@ -1,5 +1,5 @@ { - "localhost:8080": { + "huly.local:8080": { "title": "Huly", "languages": "en,ru,pt,es,zh,fr,de", "defaultLanguage": "en", @@ -28,7 +28,7 @@ } ] }, - "localhost:8087": { + "huly.local:8087": { "title": "Huly", "languages": "en,ru,pt,es,zh,fr,de", "defaultLanguage": "en", @@ -57,7 +57,7 @@ } ] }, - "localhost:8081": { + "huly.local:8081": { "title": "TraceX", "languages": "en", "defaultLanguage": "en", @@ -97,7 +97,7 @@ } ] }, - "localhost:8088": { + "huly.local:8088": { "title": "TraceX", "languages": "en", "defaultLanguage": "en", diff --git a/dev/scripts/debug_account.sh b/dev/scripts/debug_account.sh index 0581bdf248..8809d4bbd8 100755 --- a/dev/scripts/debug_account.sh +++ b/dev/scripts/debug_account.sh @@ -9,14 +9,14 @@ echo "Running account on port: ${port}" #MONGO_URL=mongodb://localhost:27017, export DB_URL="mongodb://localhost:27018" # DB_URL=postgresql://postgres:example@localhost:5432, -# DB_URL=postgresql://root@host.docker.internal:26257/defaultdb?sslmode=disable, +# DB_URL=postgresql://root@huly.local:26257/defaultdb?sslmode=disable, export SERVER_SECRET="secret" export REGION_INFO="|America;europe|" export TRANSACTOR_URL="ws://transactor:3334;ws://localhost:3334,ws://transactor-europe:3335;ws://localhost:3335;europe," export ACCOUNTS_URL="http://localhost:${port}" export ACCOUNT_PORT=${port} export FRONT_URL="http://localhost:8080" -export STATS_URL="http://host.docker.internal:4900" +export STATS_URL="http://huly.local:4900" export SES_URL= export MINIO_ACCESS_KEY="minioadmin" export MINIO_SECRET_KEY="minioadmin" diff --git a/dev/tool/src/index.ts b/dev/tool/src/index.ts index 358d39be10..7d532d0da7 100644 --- a/dev/tool/src/index.ts +++ b/dev/tool/src/index.ts @@ -279,7 +279,11 @@ export function devTool ( throw new Error(`Workspace ${workspace} not found`) } - await assignWorkspace(toolCtx, db, null, getToolToken(), email, ws.uuid, AccountRole.User) + await assignWorkspace(toolCtx, db, null, getToolToken(), { + email, + workspaceUuid: ws.uuid, + role: AccountRole.User + }) } catch (err: any) { console.error(err) } @@ -341,16 +345,12 @@ export function devTool ( undefined, true ) - await updateWorkspaceInfo( - measureCtx, - db, - brandingObj, - getToolToken(), - res.workspaceUuid, - 'create-done', + await updateWorkspaceInfo(measureCtx, db, brandingObj, getToolToken(), { + workspaceUuid: res.workspaceUuid, + event: 'create-done', version, - 100 - ) + progress: 100 + }) console.log('create-workspace done') }) @@ -372,7 +372,7 @@ export function devTool ( throw new Error(`Workspace ${workspace} not found`) } - await assignWorkspace(toolCtx, db, null, getToolToken(), email, ws.uuid, role) + await assignWorkspace(toolCtx, db, null, getToolToken(), { email, workspaceUuid: ws.uuid, role }) }) }) @@ -423,7 +423,12 @@ export function devTool ( true ) - await updateWorkspaceInfo(measureCtx, db, null, getToolToken(), info.uuid, 'upgrade-done', version, 100) + await updateWorkspaceInfo(measureCtx, db, null, getToolToken(), { + workspaceUuid: info.uuid, + event: 'upgrade-done', + version, + progress: 100 + }) console.log(metricsToString(measureCtx.metrics, 'upgrade', 60)) console.log('upgrade-workspace done') diff --git a/packages/account-client/src/client.ts b/packages/account-client/src/client.ts index dfc8e64f33..089e33001b 100644 --- a/packages/account-client/src/client.ts +++ b/packages/account-client/src/client.ts @@ -107,6 +107,9 @@ export interface AccountClient { assignWorkspace: (email: string, workspaceUuid: string, role: AccountRole) => Promise updateBackupInfo: (info: BackupStatus) => Promise updateWorkspaceRoleBySocialId: (socialKey: string, targetRole: AccountRole) => Promise + + setCookie: () => Promise + deleteCookie: () => Promise } /** @public */ @@ -120,10 +123,12 @@ export function getClient (accountsUrl?: string, token?: string): AccountClient interface Request { method: string - params: any[] + params: Record } class AccountClientImpl implements AccountClient { + private readonly request: RequestInit + constructor ( private readonly url: string, private readonly token?: string @@ -131,6 +136,18 @@ class AccountClientImpl implements AccountClient { if (url === '') { throw new Error('Accounts url not specified') } + + this.request = { + keepalive: true, + headers: { + ...(this.token === undefined + ? {} + : { + Authorization: 'Bearer ' + this.token + }) + }, + credentials: 'include' + } } async getProviders (): Promise { @@ -143,16 +160,12 @@ class AccountClientImpl implements AccountClient { private async rpc(request: Request): Promise { const response = await fetch(this.url, { - method: 'POST', - keepalive: true, + ...this.request, headers: { - ...(this.token === undefined - ? {} - : { - Authorization: 'Bearer ' + this.token - }), + ...this.request.headers, 'Content-Type': 'application/json' }, + method: 'POST', body: JSON.stringify(request) }) @@ -183,7 +196,7 @@ class AccountClientImpl implements AccountClient { async getUserWorkspaces (): Promise { const request = { method: 'getUserWorkspaces' as const, - params: [] + params: {} } return (await this.rpc(request)).map((ws) => this.flattenStatus(ws)) @@ -196,7 +209,7 @@ class AccountClientImpl implements AccountClient { ): Promise { const request = { method: 'selectWorkspace' as const, - params: [workspaceUrl, kind, externalRegions] + params: { workspaceUrl, kind, externalRegions } } return await this.rpc(request) @@ -205,7 +218,7 @@ class AccountClientImpl implements AccountClient { async validateOtp (email: string, code: string): Promise { const request = { method: 'validateOtp' as const, - params: [email, code] + params: { email, code } } return await this.rpc(request) @@ -214,7 +227,7 @@ class AccountClientImpl implements AccountClient { async loginOtp (email: string): Promise { const request = { method: 'loginOtp' as const, - params: [email] + params: { email } } return await this.rpc(request) @@ -223,7 +236,7 @@ class AccountClientImpl implements AccountClient { async getLoginInfoByToken (): Promise { const request = { method: 'getLoginInfoByToken' as const, - params: [] + params: {} } return await this.rpc(request) @@ -232,7 +245,7 @@ class AccountClientImpl implements AccountClient { async restorePassword (password: string): Promise { const request = { method: 'restorePassword' as const, - params: [password] + params: { password } } return await this.rpc(request) @@ -241,7 +254,7 @@ class AccountClientImpl implements AccountClient { async confirm (): Promise { const request = { method: 'confirm' as const, - params: [] + params: {} } return await this.rpc(request) @@ -250,7 +263,7 @@ class AccountClientImpl implements AccountClient { async requestPasswordReset (email: string): Promise { const request = { method: 'requestPasswordReset' as const, - params: [email] + params: { email } } await this.rpc(request) @@ -259,7 +272,7 @@ class AccountClientImpl implements AccountClient { async sendInvite (email: string, role: AccountRole): Promise { const request = { method: 'sendInvite' as const, - params: [email, role] + params: { email, role } } await this.rpc(request) @@ -277,7 +290,7 @@ class AccountClientImpl implements AccountClient { async leaveWorkspace (account: string): Promise { const request = { method: 'leaveWorkspace' as const, - params: [account] + params: { account } } return await this.rpc(request) @@ -286,7 +299,7 @@ class AccountClientImpl implements AccountClient { async changeUsername (first: string, last: string): Promise { const request = { method: 'changeUsername' as const, - params: [first, last] + params: { first, last } } await this.rpc(request) @@ -295,7 +308,7 @@ class AccountClientImpl implements AccountClient { async changePassword (oldPassword: string, newPassword: string): Promise { const request = { method: 'changePassword' as const, - params: [oldPassword, newPassword] + params: { oldPassword, newPassword } } await this.rpc(request) @@ -310,7 +323,7 @@ class AccountClientImpl implements AccountClient { ): Promise { const request = { method: 'signUpJoin' as const, - params: [email, password, first, last, inviteId] + params: { email, password, first, last, inviteId } } return await this.rpc(request) @@ -319,7 +332,7 @@ class AccountClientImpl implements AccountClient { async join (email: string, password: string, inviteId: string): Promise { const request = { method: 'join' as const, - params: [email, password, inviteId] + params: { email, password, inviteId } } return await this.rpc(request) @@ -334,7 +347,7 @@ class AccountClientImpl implements AccountClient { ): Promise { const request = { method: 'createInviteLink' as const, - params: [exp, emailMask, limit, role, personId] + params: { exp, emailMask, limit, role, personId } } return await this.rpc(request) @@ -343,7 +356,7 @@ class AccountClientImpl implements AccountClient { async checkJoin (inviteId: string): Promise { const request = { method: 'checkJoin' as const, - params: [inviteId] + params: { inviteId } } return await this.rpc(request) @@ -352,7 +365,7 @@ class AccountClientImpl implements AccountClient { async getWorkspaceInfo (updateLastVisit: boolean = false): Promise { const request = { method: 'getWorkspaceInfo' as const, - params: updateLastVisit ? [true] : [] + params: updateLastVisit ? { updateLastVisit: true } : {} } return this.flattenStatus(await this.rpc(request)) @@ -361,34 +374,34 @@ class AccountClientImpl implements AccountClient { async getRegionInfo (): Promise { const request = { method: 'getRegionInfo' as const, - params: [] + params: {} } return await this.rpc(request) } - async createWorkspace (name: string, region?: string): Promise { + async createWorkspace (workspaceName: string, region?: string): Promise { const request = { method: 'createWorkspace' as const, - params: [name, region] + params: { workspaceName, region } } return await this.rpc(request) } - async signUpOtp (email: string, first: string, last: string): Promise { + async signUpOtp (email: string, firstName: string, lastName: string): Promise { const request = { method: 'signUpOtp' as const, - params: [email, first, last] + params: { email, firstName, lastName } } return await this.rpc(request) } - async signUp (email: string, password: string, first: string, last: string): Promise { + async signUp (email: string, password: string, firstName: string, lastName: string): Promise { const request = { method: 'signUp' as const, - params: [email, password, first, last] + params: { email, password, firstName, lastName } } return await this.rpc(request) @@ -397,7 +410,7 @@ class AccountClientImpl implements AccountClient { async login (email: string, password: string): Promise { const request = { method: 'login' as const, - params: [email, password] + params: { email, password } } return await this.rpc(request) @@ -406,7 +419,7 @@ class AccountClientImpl implements AccountClient { async getPerson (): Promise { const request = { method: 'getPerson' as const, - params: [] + params: {} } return await this.rpc(request) @@ -415,7 +428,7 @@ class AccountClientImpl implements AccountClient { async getPersonInfo (account: PersonUuid): Promise { const request = { method: 'getPersonInfo' as const, - params: [account] + params: { account } } return await this.rpc(request) @@ -424,7 +437,7 @@ class AccountClientImpl implements AccountClient { async getSocialIds (): Promise { const request = { method: 'getSocialIds' as const, - params: [] + params: {} } return await this.rpc(request) @@ -433,7 +446,7 @@ class AccountClientImpl implements AccountClient { async workerHandshake (region: string, version: Data, operation: WorkspaceOperation): Promise { const request = { method: 'workerHandshake' as const, - params: [region, version, operation] + params: { region, version, operation } } await this.rpc(request) @@ -446,7 +459,7 @@ class AccountClientImpl implements AccountClient { ): Promise { const request = { method: 'getPendingWorkspace' as const, - params: [region, version, operation] + params: { region, version, operation } } const result = await this.rpc(request) @@ -458,7 +471,7 @@ class AccountClientImpl implements AccountClient { } async updateWorkspaceInfo ( - wsUuid: string, + workspaceUuid: string, event: string, version: Data, progress: number, @@ -466,7 +479,7 @@ class AccountClientImpl implements AccountClient { ): Promise { const request = { method: 'updateWorkspaceInfo' as const, - params: [wsUuid, event, version, progress, message] + params: { workspaceUuid, event, version, progress, message } } await this.rpc(request) @@ -475,16 +488,16 @@ class AccountClientImpl implements AccountClient { async getWorkspaceMembers (): Promise { const request = { method: 'getWorkspaceMembers' as const, - params: [] + params: {} } return await this.rpc(request) } - async updateWorkspaceRole (account: string, role: AccountRole): Promise { + async updateWorkspaceRole (targetAccount: string, targetRole: AccountRole): Promise { const request = { method: 'updateWorkspaceRole' as const, - params: [account, role] + params: { targetAccount, targetRole } } await this.rpc(request) @@ -493,7 +506,7 @@ class AccountClientImpl implements AccountClient { async updateWorkspaceName (name: string): Promise { const request = { method: 'updateWorkspaceName' as const, - params: [name] + params: { name } } await this.rpc(request) @@ -502,7 +515,7 @@ class AccountClientImpl implements AccountClient { async deleteWorkspace (): Promise { const request = { method: 'deleteWorkspace' as const, - params: [] + params: {} } await this.rpc(request) @@ -511,7 +524,7 @@ class AccountClientImpl implements AccountClient { async findPerson (socialString: string): Promise { const request = { method: 'findPerson' as const, - params: [socialString] + params: { socialString } } return await this.rpc(request) @@ -520,7 +533,7 @@ class AccountClientImpl implements AccountClient { async listWorkspaces (region?: string | null, mode: WorkspaceMode | null = null): Promise { const request = { method: 'listWorkspaces' as const, - params: [region, mode] + params: { region, mode } } return ((await this.rpc(request)) ?? []).map((ws) => this.flattenStatus(ws)) @@ -533,16 +546,16 @@ class AccountClientImpl implements AccountClient { ): Promise { const request = { method: 'performWorkspaceOperation' as const, - params: [workspaceId, event, ...params] + params: { workspaceId, event, params } } return await this.rpc(request) } - async updateBackupInfo (info: BackupStatus): Promise { + async updateBackupInfo (backupInfo: BackupStatus): Promise { const request = { method: 'updateBackupInfo' as const, - params: [info] + params: { backupInfo } } await this.rpc(request) @@ -551,7 +564,7 @@ class AccountClientImpl implements AccountClient { async assignWorkspace (email: string, workspaceUuid: string, role: AccountRole): Promise { const request = { method: 'assignWorkspace' as const, - params: [email, workspaceUuid, role] + params: { email, workspaceUuid, role } } await this.rpc(request) @@ -560,11 +573,35 @@ class AccountClientImpl implements AccountClient { async updateWorkspaceRoleBySocialId (socialKey: string, targetRole: AccountRole): Promise { const request = { method: 'updateWorkspaceRoleBySocialId' as const, - params: [socialKey, targetRole] + params: { socialKey, targetRole } } await this.rpc(request) } + + async setCookie (): Promise { + const url = concatLink(this.url, '/cookie') + const response = await fetch(url, { ...this.request, method: 'PUT' }) + + if (!response.ok) { + const result = await response.json() + if (result.error != null) { + throw new PlatformError(result.error) + } + } + } + + async deleteCookie (): Promise { + const url = concatLink(this.url, '/cookie') + const response = await fetch(url, { ...this.request, method: 'DELETE' }) + + if (!response.ok) { + const result = await response.json() + if (result.error != null) { + throw new PlatformError(result.error) + } + } + } } async function retry (retries: number, op: () => Promise, delay: number = 100): Promise { diff --git a/plugins/guest-resources/src/connect.ts b/plugins/guest-resources/src/connect.ts index a6a099f126..ffad8416a5 100644 --- a/plugins/guest-resources/src/connect.ts +++ b/plugins/guest-resources/src/connect.ts @@ -15,20 +15,14 @@ import { setCurrentEmployee, type Employee } from '@hcengineering/contact' import login, { loginId } from '@hcengineering/login' import { getMetadata, getResource, setMetadata } from '@hcengineering/platform' import presentation, { - closeClient, loadServerConfig, refreshClient, setClient, setPresentationCookie, upgradeDownloadProgress } from '@hcengineering/presentation' -import { - desktopPlatform, - fetchMetadataLocalStorage, - getCurrentLocation, - navigate, - setMetadataLocalStorage -} from '@hcengineering/ui' +import { desktopPlatform, getCurrentLocation, navigate } from '@hcengineering/ui' +import { logOut } from '@hcengineering/workbench' import { writable, get } from 'svelte/store' export const versionError = writable(undefined) @@ -48,7 +42,6 @@ export async function connect (title: string): Promise { }) return } - setMetadata(presentation.metadata.Token, token) const selectWorkspace = await getResource(login.function.SelectWorkspace) const workspaceLoginInfo = (await selectWorkspace(wsUrl, token))[1] @@ -57,10 +50,8 @@ export async function connect (title: string): Promise { `Error selecting workspace ${wsUrl}. There might be something wrong with the token. Please try to log in again.` ) // something went wrong with selecting workspace with the selected token - clearMetadata(wsUrl) - navigate({ - path: [loginId] - }) + await logOut() + navigate({ path: [loginId] }) return } @@ -126,10 +117,11 @@ export async function connect (title: string): Promise { location.reload() }, onUnauthorized: () => { - clearMetadata(wsUrl) - navigate({ - path: [loginId], - query: {} + void logOut().then(() => { + navigate({ + path: [loginId], + query: {} + }) }) }, // We need to refresh all active live queries and clear old queries. @@ -240,22 +232,3 @@ export async function connect (title: string): Promise { return _client } - -function clearMetadata (ws: string): void { - const tokens = fetchMetadataLocalStorage(login.metadata.LoginTokensV2) - if (tokens !== null) { - const loc = getCurrentLocation() - // eslint-disable-next-line - delete tokens[loc.path[1]] - setMetadataLocalStorage(login.metadata.LoginTokensV2, tokens) - } - const currentWorkspace = getMetadata(presentation.metadata.WorkspaceUuid) - if (currentWorkspace !== undefined) { - setPresentationCookie('', currentWorkspace) - } - - setMetadata(presentation.metadata.Token, null) - setMetadataLocalStorage(login.metadata.LastToken, null) - setMetadataLocalStorage(login.metadata.LoginAccount, null) - void closeClient() -} diff --git a/plugins/guest-resources/src/utils.ts b/plugins/guest-resources/src/utils.ts index 7c98303d1c..30e15b4653 100644 --- a/plugins/guest-resources/src/utils.ts +++ b/plugins/guest-resources/src/utils.ts @@ -1,9 +1,9 @@ import client from '@hcengineering/client' -import { type Doc } from '@hcengineering/core' +import { type Doc, AccountRole } from '@hcengineering/core' import login from '@hcengineering/login' -import { getMetadata, getResource, setMetadata } from '@hcengineering/platform' +import { getMetadata, getResource } from '@hcengineering/platform' import presentation from '@hcengineering/presentation' -import { fetchMetadataLocalStorage, getCurrentLocation, navigate } from '@hcengineering/ui' +import { getCurrentLocation, navigate } from '@hcengineering/ui' import view from '@hcengineering/view' import { getObjectLinkFragment } from '@hcengineering/view-resources' import { workbenchId } from '@hcengineering/workbench' @@ -11,10 +11,15 @@ import { workbenchId } from '@hcengineering/workbench' export async function checkAccess (doc: Doc): Promise { const loc = getCurrentLocation() const ws = loc.path[1] - const tokens: Record = fetchMetadataLocalStorage(login.metadata.LoginTokensV2) ?? {} - const token = tokens[ws] + + const selectWorkspace = await getResource(login.function.SelectWorkspace) + const wsLoginInfo = (await selectWorkspace(ws, null))[1] + if (wsLoginInfo === undefined || wsLoginInfo.role === AccountRole.DocGuest) return + + const token = wsLoginInfo.token const endpoint = getMetadata(presentation.metadata.Endpoint) if (token === undefined || endpoint === undefined) return + const clientFactory = await getResource(client.function.GetClient) const _client = await clientFactory(token, endpoint) @@ -28,7 +33,7 @@ export async function checkAccess (doc: Doc): Promise { loc.path[0] = workbenchId loc.path[1] = ws // We have access, let's set correct tokens and redirect) - setMetadata(presentation.metadata.Token, token) + // setMetadata(presentation.metadata.Token, token) navigate(loc) } } diff --git a/plugins/login-resources/src/actions.ts b/plugins/login-resources/src/actions.ts index 7dbb32e473..36905f800e 100644 --- a/plugins/login-resources/src/actions.ts +++ b/plugins/login-resources/src/actions.ts @@ -1,7 +1,6 @@ import { goTo } from './utils' import login from './plugin' import { type BottomAction } from '.' -import { setMetadataLocalStorage } from '@hcengineering/ui' import { setMetadata } from '@hcengineering/platform' import presentation from '@hcengineering/presentation' @@ -20,7 +19,6 @@ export const loginAction: BottomAction = { page: 'login', func: () => { setMetadata(presentation.metadata.Token, null) - setMetadataLocalStorage(login.metadata.LastToken, null) goTo('login', true) } } diff --git a/plugins/login-resources/src/components/Auth.svelte b/plugins/login-resources/src/components/Auth.svelte index d191e14bf2..25d589a004 100644 --- a/plugins/login-resources/src/components/Auth.svelte +++ b/plugins/login-resources/src/components/Auth.svelte @@ -1,24 +1,21 @@