mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-21 16:09:12 +03:00
EZQMS-951: Server branding (#5858)
Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com>
This commit is contained in:
parent
ed95c59859
commit
dedff23b31
@ -285,7 +285,8 @@ export function devTool (
|
||||
.requiredOption('-w, --workspaceName <workspaceName>', 'Workspace name')
|
||||
.option('-e, --email <email>', 'Author email', 'platform@email.com')
|
||||
.option('-i, --init <ws>', 'Init from workspace')
|
||||
.action(async (workspace, cmd: { email: string, workspaceName: string, init?: string }) => {
|
||||
.option('-b, --branding <key>', 'Branding key')
|
||||
.action(async (workspace, cmd: { email: string, workspaceName: string, init?: string, branding?: string }) => {
|
||||
const { mongodbUri, txes, version, migrateOperations } = prepareTools()
|
||||
await withDatabase(mongodbUri, async (db) => {
|
||||
await createWorkspace(
|
||||
@ -295,7 +296,9 @@ export function devTool (
|
||||
migrateOperations,
|
||||
db,
|
||||
productId,
|
||||
cmd.init !== undefined ? { initWorkspace: cmd.init } : null,
|
||||
cmd.init !== undefined || cmd.branding !== undefined
|
||||
? { initWorkspace: cmd.init, key: cmd.branding ?? 'huly' }
|
||||
: null,
|
||||
cmd.email,
|
||||
cmd.workspaceName,
|
||||
workspace
|
||||
|
@ -661,6 +661,7 @@ export interface BaseWorkspaceInfo {
|
||||
productId: string
|
||||
disabled?: boolean
|
||||
version?: Data<Version>
|
||||
branding?: string
|
||||
|
||||
workspaceUrl?: string | null // An optional url to the workspace, if not set workspace will be used
|
||||
workspaceName?: string // An displayed workspace name
|
||||
|
@ -67,3 +67,14 @@ export interface LowLevelStorage {
|
||||
// Remove a list of documents.
|
||||
clean: (ctx: MeasureContext, domain: Domain, docs: Ref<Doc>[]) => Promise<void>
|
||||
}
|
||||
|
||||
export interface Branding {
|
||||
key?: string
|
||||
front?: string
|
||||
title?: string
|
||||
language?: string
|
||||
initWorkspace?: string
|
||||
lastNameFirst?: string
|
||||
}
|
||||
|
||||
export type BrandingMap = Record<string, Branding>
|
||||
|
@ -19,7 +19,8 @@ import {
|
||||
type MeasureContext,
|
||||
type Ref,
|
||||
type WorkspaceId,
|
||||
type WorkspaceIdWithUrl
|
||||
type WorkspaceIdWithUrl,
|
||||
type Branding
|
||||
} from '@hcengineering/core'
|
||||
import type { BlobLookup } from '@hcengineering/core/src/classes'
|
||||
import { type Readable } from 'stream'
|
||||
@ -84,7 +85,12 @@ export interface StorageAdapter {
|
||||
) => Promise<Readable>
|
||||
|
||||
// Lookup will extend Blob with lookup information.
|
||||
lookup: (ctx: MeasureContext, workspaceId: WorkspaceIdWithUrl, docs: Blob[]) => Promise<BlobLookupResult>
|
||||
lookup: (
|
||||
ctx: MeasureContext,
|
||||
workspaceId: WorkspaceIdWithUrl,
|
||||
branding: Branding | null,
|
||||
docs: Blob[]
|
||||
) => Promise<BlobLookupResult>
|
||||
}
|
||||
|
||||
export interface StorageAdapterEx extends StorageAdapter {
|
||||
@ -174,7 +180,12 @@ export class DummyStorageAdapter implements StorageAdapter, StorageAdapterEx {
|
||||
throw new Error('not implemented')
|
||||
}
|
||||
|
||||
async lookup (ctx: MeasureContext, workspaceId: WorkspaceIdWithUrl, docs: Blob[]): Promise<BlobLookupResult> {
|
||||
async lookup (
|
||||
ctx: MeasureContext,
|
||||
workspaceId: WorkspaceIdWithUrl,
|
||||
branding: Branding | null,
|
||||
docs: Blob[]
|
||||
): Promise<BlobLookupResult> {
|
||||
return { lookups: [] }
|
||||
}
|
||||
}
|
||||
|
@ -211,8 +211,10 @@ export function getLastName (name: string): string {
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function formatName (name: string): string {
|
||||
return getMetadata(contactPlugin.metadata.LastNameFirst) === true
|
||||
export function formatName (name: string, lastNameFirst?: string): string {
|
||||
const lastNameFirstCombined =
|
||||
lastNameFirst !== undefined ? lastNameFirst === 'true' : getMetadata(contactPlugin.metadata.LastNameFirst) === true
|
||||
return lastNameFirstCombined
|
||||
? getLastName(name) + ' ' + getFirstName(name)
|
||||
: getFirstName(name) + ' ' + getLastName(name)
|
||||
}
|
||||
@ -220,9 +222,9 @@ export function formatName (name: string): string {
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function getName (hierarchy: Hierarchy, value: Contact): string {
|
||||
export function getName (hierarchy: Hierarchy, value: Contact, lastNameFirst?: string): string {
|
||||
if (isPerson(hierarchy, value)) {
|
||||
return formatName(value.name)
|
||||
return formatName(value.name, lastNameFirst)
|
||||
}
|
||||
return value.name
|
||||
}
|
||||
@ -238,9 +240,14 @@ function isPersonClass (hierarchy: Hierarchy, _class: Ref<Class<Doc>>): boolean
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function formatContactName (hierarchy: Hierarchy, _class: Ref<Class<Doc>>, name: string): string {
|
||||
export function formatContactName (
|
||||
hierarchy: Hierarchy,
|
||||
_class: Ref<Class<Doc>>,
|
||||
name: string,
|
||||
lastNameFirst?: string
|
||||
): string {
|
||||
if (isPersonClass(hierarchy, _class)) {
|
||||
return formatName(name)
|
||||
return formatName(name, lastNameFirst)
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
@ -62,6 +62,7 @@
|
||||
"@koa/cors": "^3.1.0",
|
||||
"@hcengineering/server-tool": "^0.6.0",
|
||||
"@hcengineering/server-token": "^0.6.11",
|
||||
"@hcengineering/server-core": "^0.6.1",
|
||||
"@hcengineering/model-all": "^0.6.0"
|
||||
}
|
||||
}
|
||||
|
@ -13,8 +13,7 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import fs from 'fs'
|
||||
import { type BrandingMap } from '@hcengineering/account'
|
||||
import { loadBrandingMap } from '@hcengineering/server-core'
|
||||
import { serveAccount } from '@hcengineering/account-service'
|
||||
import { MeasureMetricsContext, newMetrics, type Tx } from '@hcengineering/core'
|
||||
import builder, { getModelVersion, migrateOperations } from '@hcengineering/model-all'
|
||||
@ -28,9 +27,4 @@ const metricsContext = new MeasureMetricsContext('account', {}, {}, newMetrics()
|
||||
|
||||
const brandingPath = process.env.BRANDING_PATH
|
||||
|
||||
let brandings: BrandingMap = {}
|
||||
if (brandingPath !== undefined && brandingPath !== '') {
|
||||
brandings = JSON.parse(fs.readFileSync(brandingPath, 'utf8'))
|
||||
}
|
||||
|
||||
serveAccount(metricsContext, getModelVersion(), txes, migrateOperations, '', brandings)
|
||||
serveAccount(metricsContext, getModelVersion(), txes, migrateOperations, '', loadBrandingMap(brandingPath))
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { joinWithProvider, loginWithProvider } from '@hcengineering/account'
|
||||
import { concatLink, MeasureContext } from '@hcengineering/core'
|
||||
import { BrandingMap, concatLink, MeasureContext } from '@hcengineering/core'
|
||||
import Router from 'koa-router'
|
||||
import { Db } from 'mongodb'
|
||||
import { Strategy as GitHubStrategy } from 'passport-github2'
|
||||
import { Passport } from '.'
|
||||
import { getBranding, getHost, safeParseAuthState } from './utils'
|
||||
|
||||
export function registerGithub (
|
||||
measureCtx: MeasureContext,
|
||||
@ -12,7 +13,8 @@ export function registerGithub (
|
||||
accountsUrl: string,
|
||||
db: Db,
|
||||
productId: string,
|
||||
frontUrl: string
|
||||
frontUrl: string,
|
||||
brandings: BrandingMap
|
||||
): string | undefined {
|
||||
const GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID
|
||||
const GITHUB_CLIENT_SECRET = process.env.GITHUB_CLIENT_SECRET
|
||||
@ -34,21 +36,39 @@ export function registerGithub (
|
||||
)
|
||||
|
||||
router.get('/auth/github', async (ctx, next) => {
|
||||
const state = ctx.query?.inviteId
|
||||
measureCtx.info('try auth via', { provider: 'github' })
|
||||
const host = getHost(ctx.request.headers)
|
||||
const branding = host !== undefined ? brandings[host]?.key ?? '' : ''
|
||||
const state = encodeURIComponent(
|
||||
JSON.stringify({
|
||||
inviteId: ctx.query?.inviteId,
|
||||
branding
|
||||
})
|
||||
)
|
||||
|
||||
passport.authenticate('github', { scope: ['user:email'], session: true, state })(ctx, next)
|
||||
})
|
||||
|
||||
router.get(
|
||||
redirectURL,
|
||||
passport.authenticate('github', { failureRedirect: concatLink(frontUrl, '/login'), session: true }),
|
||||
async (ctx, next) => {
|
||||
const state = safeParseAuthState(ctx.query?.state)
|
||||
const branding = getBranding(brandings, state?.branding)
|
||||
|
||||
passport.authenticate('github', {
|
||||
failureRedirect: concatLink(branding?.front ?? frontUrl, '/login'),
|
||||
session: true
|
||||
})(ctx, next)
|
||||
},
|
||||
async (ctx, next) => {
|
||||
try {
|
||||
const email = ctx.state.user.emails?.[0]?.value ?? `github:${ctx.state.user.username}`
|
||||
const [first, last] = ctx.state.user.displayName?.split(' ') ?? [ctx.state.user.username, '']
|
||||
measureCtx.info('Provider auth handler', { email, type: 'github' })
|
||||
if (email !== undefined) {
|
||||
if (ctx.query?.state != null) {
|
||||
const state = safeParseAuthState(ctx.query?.state)
|
||||
const branding = getBranding(brandings, state?.branding)
|
||||
if (state.inviteId != null && state.inviteId !== '') {
|
||||
const loginInfo = await joinWithProvider(
|
||||
measureCtx,
|
||||
db,
|
||||
@ -57,7 +77,7 @@ export function registerGithub (
|
||||
email,
|
||||
first,
|
||||
last,
|
||||
ctx.query.state,
|
||||
state.inviteId as any,
|
||||
{
|
||||
githubId: ctx.state.user.id
|
||||
}
|
||||
@ -75,7 +95,7 @@ export function registerGithub (
|
||||
}
|
||||
measureCtx.info('Success auth, redirect', { email, type: 'github' })
|
||||
// Successful authentication, redirect to your application
|
||||
ctx.redirect(concatLink(frontUrl, '/login/auth'))
|
||||
ctx.redirect(concatLink(branding?.front ?? frontUrl, '/login/auth'))
|
||||
}
|
||||
} catch (err: any) {
|
||||
measureCtx.error('failed to auth', { err, type: 'github', user: ctx.state?.user })
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { joinWithProvider, loginWithProvider } from '@hcengineering/account'
|
||||
import { concatLink, MeasureContext } from '@hcengineering/core'
|
||||
import { BrandingMap, concatLink, MeasureContext } from '@hcengineering/core'
|
||||
import Router from 'koa-router'
|
||||
import { Db } from 'mongodb'
|
||||
import { Strategy as GoogleStrategy } from 'passport-google-oauth20'
|
||||
import { Passport } from '.'
|
||||
import { getBranding, getHost, safeParseAuthState } from './utils'
|
||||
|
||||
export function registerGoogle (
|
||||
measureCtx: MeasureContext,
|
||||
@ -12,7 +13,8 @@ export function registerGoogle (
|
||||
accountsUrl: string,
|
||||
db: Db,
|
||||
productId: string,
|
||||
frontUrl: string
|
||||
frontUrl: string,
|
||||
brandings: BrandingMap
|
||||
): string | undefined {
|
||||
const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID
|
||||
const GOOGLE_CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET
|
||||
@ -34,14 +36,30 @@ export function registerGoogle (
|
||||
)
|
||||
|
||||
router.get('/auth/google', async (ctx, next) => {
|
||||
const state = ctx.query?.inviteId
|
||||
measureCtx.info('try auth via', { provider: 'google' })
|
||||
const host = getHost(ctx.request.headers)
|
||||
const branding = host !== undefined ? brandings[host]?.key ?? '' : ''
|
||||
const state = encodeURIComponent(
|
||||
JSON.stringify({
|
||||
inviteId: ctx.query?.inviteId,
|
||||
branding
|
||||
})
|
||||
)
|
||||
|
||||
passport.authenticate('google', { scope: ['profile', 'email'], session: true, state })(ctx, next)
|
||||
})
|
||||
|
||||
router.get(
|
||||
redirectURL,
|
||||
passport.authenticate('google', { failureRedirect: concatLink(frontUrl, '/login'), session: true }),
|
||||
async (ctx, next) => {
|
||||
const state = safeParseAuthState(ctx.query?.state)
|
||||
const branding = getBranding(brandings, state?.branding)
|
||||
|
||||
passport.authenticate('google', {
|
||||
failureRedirect: concatLink(branding?.front ?? frontUrl, '/login'),
|
||||
session: true
|
||||
})(ctx, next)
|
||||
},
|
||||
async (ctx, next) => {
|
||||
const email = ctx.state.user.emails?.[0]?.value
|
||||
const first = ctx.state.user.name.givenName
|
||||
@ -49,7 +67,9 @@ export function registerGoogle (
|
||||
measureCtx.info('Provider auth handler', { email, type: 'google' })
|
||||
if (email !== undefined) {
|
||||
try {
|
||||
if (ctx.query?.state != null) {
|
||||
const state = safeParseAuthState(ctx.query?.state)
|
||||
const branding = getBranding(brandings, state?.branding)
|
||||
if (state.inviteId != null && state.inviteId !== '') {
|
||||
const loginInfo = await joinWithProvider(
|
||||
measureCtx,
|
||||
db,
|
||||
@ -58,7 +78,7 @@ export function registerGoogle (
|
||||
email,
|
||||
first,
|
||||
last,
|
||||
ctx.query.state
|
||||
state.inviteId as any
|
||||
)
|
||||
if (ctx.session != null) {
|
||||
ctx.session.loginInfo = loginInfo
|
||||
@ -72,7 +92,7 @@ export function registerGoogle (
|
||||
|
||||
// Successful authentication, redirect to your application
|
||||
measureCtx.info('Success auth, redirect', { email, type: 'google' })
|
||||
ctx.redirect(concatLink(frontUrl, '/login/auth'))
|
||||
ctx.redirect(concatLink(branding?.front ?? frontUrl, '/login/auth'))
|
||||
} catch (err: any) {
|
||||
measureCtx.error('failed to auth', { err, type: 'google', user: ctx.state?.user })
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import session from 'koa-session'
|
||||
import { Db } from 'mongodb'
|
||||
import { registerGithub } from './github'
|
||||
import { registerGoogle } from './google'
|
||||
import { MeasureContext } from '@hcengineering/core'
|
||||
import { BrandingMap, MeasureContext } from '@hcengineering/core'
|
||||
|
||||
export type Passport = typeof passport
|
||||
|
||||
@ -16,7 +16,8 @@ export type AuthProvider = (
|
||||
accountsUrl: string,
|
||||
db: Db,
|
||||
productId: string,
|
||||
frontUrl: string
|
||||
frontUrl: string,
|
||||
brandings: BrandingMap
|
||||
) => string | undefined
|
||||
|
||||
export function registerProviders (
|
||||
@ -26,7 +27,8 @@ export function registerProviders (
|
||||
db: Db,
|
||||
productId: string,
|
||||
serverSecret: string,
|
||||
frontUrl: string | undefined
|
||||
frontUrl: string | undefined,
|
||||
brandings: BrandingMap
|
||||
): void {
|
||||
const accountsUrl = process.env.ACCOUNTS_URL
|
||||
if (accountsUrl === undefined) {
|
||||
@ -60,7 +62,7 @@ export function registerProviders (
|
||||
const res: string[] = []
|
||||
const providers: AuthProvider[] = [registerGoogle, registerGithub]
|
||||
for (const provider of providers) {
|
||||
const value = provider(ctx, passport, router, accountsUrl, db, productId, frontUrl)
|
||||
const value = provider(ctx, passport, router, accountsUrl, db, productId, frontUrl, brandings)
|
||||
if (value !== undefined) res.push(value)
|
||||
}
|
||||
|
||||
|
49
pods/authProviders/src/utils.ts
Normal file
49
pods/authProviders/src/utils.ts
Normal file
@ -0,0 +1,49 @@
|
||||
//
|
||||
// Copyright © 2024 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 { Branding, BrandingMap } from '@hcengineering/core'
|
||||
import { IncomingHttpHeaders } from 'http'
|
||||
|
||||
export function getHost (headers: IncomingHttpHeaders): string | undefined {
|
||||
let host: string | undefined
|
||||
const origin = headers.origin ?? headers.referer
|
||||
if (origin !== undefined) {
|
||||
host = new URL(origin).host
|
||||
}
|
||||
|
||||
return host
|
||||
}
|
||||
|
||||
export function getBranding (brandings: BrandingMap, key: string | undefined): Branding | null {
|
||||
if (key === undefined) return null
|
||||
|
||||
return Object.values(brandings).find((branding) => branding.key === key) ?? null
|
||||
}
|
||||
|
||||
export interface AuthState {
|
||||
inviteId?: string
|
||||
branding?: string
|
||||
}
|
||||
|
||||
export function safeParseAuthState (rawState: string | undefined): AuthState {
|
||||
if (rawState == null) {
|
||||
return {}
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(decodeURIComponent(rawState))
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
@ -20,7 +20,7 @@ import notification from '@hcengineering/notification'
|
||||
import { setMetadata } from '@hcengineering/platform'
|
||||
import { serverConfigFromEnv } from '@hcengineering/server'
|
||||
import { storageConfigFromEnv } from '@hcengineering/server-storage'
|
||||
import serverCore, { type StorageConfiguration } from '@hcengineering/server-core'
|
||||
import serverCore, { type StorageConfiguration, loadBrandingMap } from '@hcengineering/server-core'
|
||||
import serverNotification from '@hcengineering/server-notification'
|
||||
import serverToken from '@hcengineering/server-token'
|
||||
import { start } from '.'
|
||||
@ -61,6 +61,7 @@ const shutdown = start(config.url, {
|
||||
indexParallel: 2,
|
||||
indexProcessing: 50,
|
||||
productId: '',
|
||||
brandingMap: loadBrandingMap(config.brandingPath),
|
||||
enableCompression: config.enableCompression,
|
||||
accountsUrl: config.accountsUrl
|
||||
})
|
||||
|
@ -22,7 +22,9 @@ import {
|
||||
DOMAIN_TRANSIENT,
|
||||
DOMAIN_TX,
|
||||
type MeasureContext,
|
||||
type WorkspaceId
|
||||
type WorkspaceId,
|
||||
type BrandingMap,
|
||||
type Branding
|
||||
} from '@hcengineering/core'
|
||||
import { createElasticAdapter, createElasticBackupDataAdapter } from '@hcengineering/elastic'
|
||||
import {
|
||||
@ -47,6 +49,7 @@ import {
|
||||
createYDocAdapter,
|
||||
getMetricsContext
|
||||
} from '@hcengineering/server'
|
||||
|
||||
import { serverActivityId } from '@hcengineering/server-activity'
|
||||
import { serverAttachmentId } from '@hcengineering/server-attachment'
|
||||
import { serverCalendarId } from '@hcengineering/server-calendar'
|
||||
@ -212,6 +215,7 @@ export function start (
|
||||
rekoniUrl: string
|
||||
port: number
|
||||
productId: string
|
||||
brandingMap: BrandingMap
|
||||
serverFactory: ServerFactory
|
||||
|
||||
indexProcessing: number // 1000
|
||||
@ -267,6 +271,7 @@ export function start (
|
||||
function createIndexStages (
|
||||
fullText: MeasureContext,
|
||||
workspace: WorkspaceId,
|
||||
branding: Branding | null,
|
||||
adapter: FullTextAdapter,
|
||||
storage: ServerStorage,
|
||||
storageAdapter: StorageAdapter,
|
||||
@ -309,7 +314,7 @@ export function start (
|
||||
stages.push(summaryStage)
|
||||
|
||||
// Push all content to elastic search
|
||||
const pushStage = new FullTextPushStage(storage, adapter, workspace)
|
||||
const pushStage = new FullTextPushStage(storage, adapter, workspace, branding)
|
||||
stages.push(pushStage)
|
||||
|
||||
// OpenAI prepare stage
|
||||
@ -324,7 +329,7 @@ export function start (
|
||||
return stages
|
||||
}
|
||||
|
||||
const pipelineFactory: PipelineFactory = (ctx, workspace, upgrade, broadcast) => {
|
||||
const pipelineFactory: PipelineFactory = (ctx, workspace, upgrade, broadcast, branding) => {
|
||||
const wsMetrics = metrics.newChild('🧲 session', {})
|
||||
const conf: DbConfiguration = {
|
||||
domains: {
|
||||
@ -369,6 +374,7 @@ export function start (
|
||||
createIndexStages(
|
||||
wsMetrics.newChild('stages', {}),
|
||||
workspace,
|
||||
branding,
|
||||
adapter,
|
||||
storage,
|
||||
storageAdapter,
|
||||
@ -392,14 +398,14 @@ export function start (
|
||||
storageFactory: externalStorage,
|
||||
workspace
|
||||
}
|
||||
return createPipeline(ctx, conf, middlewares, upgrade, broadcast)
|
||||
return createPipeline(ctx, conf, middlewares, upgrade, broadcast, branding)
|
||||
}
|
||||
|
||||
const sessionFactory = (token: Token, pipeline: Pipeline): Session => {
|
||||
if (token.extra?.mode === 'backup') {
|
||||
return new BackupClientSession(token, pipeline)
|
||||
return new BackupClientSession(token, pipeline, opt.brandingMap)
|
||||
}
|
||||
return new ClientSession(token, pipeline)
|
||||
return new ClientSession(token, pipeline, opt.brandingMap)
|
||||
}
|
||||
|
||||
const onClose = startJsonRpc(getMetricsContext(), {
|
||||
@ -407,6 +413,7 @@ export function start (
|
||||
sessionFactory,
|
||||
port: opt.port,
|
||||
productId: opt.productId,
|
||||
brandingMap: opt.brandingMap,
|
||||
serverFactory: opt.serverFactory,
|
||||
enableCompression: opt.enableCompression,
|
||||
accountsUrl: opt.accountsUrl,
|
||||
|
@ -54,7 +54,7 @@ import { NOTIFICATION_BODY_SIZE } from '@hcengineering/server-notification'
|
||||
*/
|
||||
export async function channelHTMLPresenter (doc: Doc, control: TriggerControl): Promise<string> {
|
||||
const channel = doc as ChunterSpace
|
||||
const front = getMetadata(serverCore.metadata.FrontUrl) ?? ''
|
||||
const front = control.branding?.front ?? getMetadata(serverCore.metadata.FrontUrl) ?? ''
|
||||
const path = `${workbenchId}/${control.workspace.workspaceUrl}/${chunterId}/${channel._id}`
|
||||
const link = concatLink(front, path)
|
||||
return `<a href='${link}'>${channel.name}</a>`
|
||||
|
@ -202,10 +202,10 @@ export async function OnChannelUpdate (tx: Tx, control: TriggerControl): Promise
|
||||
*/
|
||||
export async function personHTMLPresenter (doc: Doc, control: TriggerControl): Promise<string> {
|
||||
const person = doc as Person
|
||||
const front = getMetadata(serverCore.metadata.FrontUrl) ?? ''
|
||||
const front = control.branding?.front ?? getMetadata(serverCore.metadata.FrontUrl) ?? ''
|
||||
const path = `${workbenchId}/${control.workspace.workspaceUrl}/${contactId}/${doc._id}`
|
||||
const link = concatLink(front, path)
|
||||
return `<a href="${link}">${getName(control.hierarchy, person)}</a>`
|
||||
return `<a href="${link}">${getName(control.hierarchy, person, control.branding?.lastNameFirst)}</a>`
|
||||
}
|
||||
|
||||
/**
|
||||
@ -213,7 +213,7 @@ export async function personHTMLPresenter (doc: Doc, control: TriggerControl): P
|
||||
*/
|
||||
export function personTextPresenter (doc: Doc, control: TriggerControl): string {
|
||||
const person = doc as Person
|
||||
return `${getName(control.hierarchy, person)}`
|
||||
return `${getName(control.hierarchy, person, control.branding?.lastNameFirst)}`
|
||||
}
|
||||
|
||||
/**
|
||||
@ -221,7 +221,7 @@ export function personTextPresenter (doc: Doc, control: TriggerControl): string
|
||||
*/
|
||||
export async function organizationHTMLPresenter (doc: Doc, control: TriggerControl): Promise<string> {
|
||||
const organization = doc as Organization
|
||||
const front = getMetadata(serverCore.metadata.FrontUrl) ?? ''
|
||||
const front = control.branding?.front ?? getMetadata(serverCore.metadata.FrontUrl) ?? ''
|
||||
const path = `${workbenchId}/${control.workspace.workspaceUrl}/${contactId}/${doc._id}`
|
||||
const link = concatLink(front, path)
|
||||
return `<a href="${link}">${organization.name}</a>`
|
||||
@ -240,7 +240,7 @@ export function organizationTextPresenter (doc: Doc): string {
|
||||
*/
|
||||
export function contactNameProvider (hierarchy: Hierarchy, props: Record<string, string>): string {
|
||||
const _class = props._class !== undefined ? (props._class as Ref<Class<Doc>>) : contact.class.Contact
|
||||
return formatContactName(hierarchy, _class, props.name ?? '')
|
||||
return formatContactName(hierarchy, _class, props.name ?? '', props.lastNameFirst)
|
||||
}
|
||||
|
||||
export async function getCurrentEmployeeName (control: TriggerControl, context: Record<string, Doc>): Promise<string> {
|
||||
@ -249,7 +249,7 @@ export async function getCurrentEmployeeName (control: TriggerControl, context:
|
||||
})
|
||||
if (account === undefined) return ''
|
||||
const employee = (await control.findAll(contact.class.Person, { _id: account.person }))[0]
|
||||
return employee !== undefined ? formatName(employee.name) : ''
|
||||
return employee !== undefined ? formatName(employee.name, control.branding?.lastNameFirst) : ''
|
||||
}
|
||||
|
||||
export async function getCurrentEmployeeEmail (control: TriggerControl, context: Record<string, Doc>): Promise<string> {
|
||||
@ -281,7 +281,7 @@ export async function getContactName (
|
||||
const value = context[contact.class.Contact] as Contact
|
||||
if (value === undefined) return
|
||||
if (control.hierarchy.isDerived(value._class, contact.class.Person)) {
|
||||
return getName(control.hierarchy, value)
|
||||
return getName(control.hierarchy, value, control.branding?.lastNameFirst)
|
||||
} else {
|
||||
return value.name
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ function getDocumentId (doc: Document): string {
|
||||
*/
|
||||
export async function documentHTMLPresenter (doc: Doc, control: TriggerControl): Promise<string> {
|
||||
const document = doc as Document
|
||||
const front = getMetadata(serverCore.metadata.FrontUrl) ?? ''
|
||||
const front = control.branding?.front ?? getMetadata(serverCore.metadata.FrontUrl) ?? ''
|
||||
const path = `${workbenchId}/${control.workspace.workspaceUrl}/${documentId}/${getDocumentId(document)}`
|
||||
const link = concatLink(front, path)
|
||||
return `<a href="${link}">${document.name}</a>`
|
||||
|
@ -14,6 +14,7 @@
|
||||
//
|
||||
|
||||
import {
|
||||
Branding,
|
||||
Doc,
|
||||
Hierarchy,
|
||||
Ref,
|
||||
@ -46,7 +47,7 @@ export async function OnPublicLinkCreate (tx: Tx, control: TriggerControl): Prom
|
||||
if (link.url !== '') return res
|
||||
|
||||
const resTx = control.txFactory.createTxUpdateDoc(link._class, link.space, link._id, {
|
||||
url: generateUrl(link._id, control.workspace)
|
||||
url: generateUrl(link._id, control.workspace, control.branding?.front)
|
||||
})
|
||||
|
||||
res.push(resTx)
|
||||
@ -54,22 +55,23 @@ export async function OnPublicLinkCreate (tx: Tx, control: TriggerControl): Prom
|
||||
return res
|
||||
}
|
||||
|
||||
export function getPublicLinkUrl (workspace: WorkspaceIdWithUrl): string {
|
||||
const front = getMetadata(serverCore.metadata.FrontUrl) ?? ''
|
||||
export function getPublicLinkUrl (workspace: WorkspaceIdWithUrl, brandedFront?: string): string {
|
||||
const front = brandedFront ?? getMetadata(serverCore.metadata.FrontUrl) ?? ''
|
||||
const path = `${guestId}/${workspace.workspaceUrl}`
|
||||
return concatLink(front, path)
|
||||
}
|
||||
|
||||
function generateUrl (linkId: Ref<PublicLink>, workspace: WorkspaceIdWithUrl): string {
|
||||
function generateUrl (linkId: Ref<PublicLink>, workspace: WorkspaceIdWithUrl, brandedFront?: string): string {
|
||||
const token = generateToken(guestAccountEmail, workspace, { linkId, guest: 'true' })
|
||||
return `${getPublicLinkUrl(workspace)}?token=${token}`
|
||||
return `${getPublicLinkUrl(workspace, brandedFront)}?token=${token}`
|
||||
}
|
||||
|
||||
export async function getPublicLink (
|
||||
doc: Doc,
|
||||
client: TxOperations,
|
||||
workspace: WorkspaceIdWithUrl,
|
||||
revokable: boolean = true
|
||||
revokable: boolean = true,
|
||||
branding: Branding | null
|
||||
): Promise<string> {
|
||||
const current = await client.findOne(guest.class.PublicLink, { attachedTo: doc._id })
|
||||
if (current !== undefined) {
|
||||
@ -79,7 +81,7 @@ export async function getPublicLink (
|
||||
return current.url
|
||||
}
|
||||
const id = generateId<PublicLink>()
|
||||
const url = generateUrl(id, workspace)
|
||||
const url = generateUrl(id, workspace, branding?.front)
|
||||
const fragment = getDocFragment(doc, client)
|
||||
await client.createDoc(
|
||||
guest.class.PublicLink,
|
||||
|
@ -278,7 +278,7 @@ async function sendEmailNotifications (
|
||||
|
||||
const senderPerson = (await control.findAll(contact.class.Person, { _id: sender.person }))[0]
|
||||
|
||||
const senderName = senderPerson !== undefined ? formatName(senderPerson.name) : ''
|
||||
const senderName = senderPerson !== undefined ? formatName(senderPerson.name, control.branding?.lastNameFirst) : ''
|
||||
const content = await getContent(doc, senderName, type, control, '')
|
||||
if (content === undefined) return
|
||||
|
||||
@ -340,7 +340,7 @@ export async function OnRequestRemove (tx: Tx, control: TriggerControl): Promise
|
||||
export async function RequestHTMLPresenter (doc: Doc, control: TriggerControl): Promise<string> {
|
||||
const request = doc as Request
|
||||
const employee = (await control.findAll(contact.mixin.Employee, { _id: request.attachedTo }))[0]
|
||||
const who = getName(control.hierarchy, employee)
|
||||
const who = getName(control.hierarchy, employee, control.branding?.lastNameFirst)
|
||||
const type = await translate(control.modelDb.getObject(request.type).label, {})
|
||||
|
||||
const date = tzDateEqual(request.tzDate, request.tzDueDate)
|
||||
@ -358,7 +358,7 @@ export async function RequestHTMLPresenter (doc: Doc, control: TriggerControl):
|
||||
export async function RequestTextPresenter (doc: Doc, control: TriggerControl): Promise<string> {
|
||||
const request = doc as Request
|
||||
const employee = (await control.findAll(contact.mixin.Employee, { _id: request.attachedTo }))[0]
|
||||
const who = getName(control.hierarchy, employee)
|
||||
const who = getName(control.hierarchy, employee, control.branding?.lastNameFirst)
|
||||
const type = await translate(control.modelDb.getObject(request.type).label, {})
|
||||
|
||||
const date = tzDateEqual(request.tzDate, request.tzDueDate)
|
||||
@ -401,7 +401,7 @@ export async function PublicHolidayHTMLPresenter (doc: Doc, control: TriggerCont
|
||||
if (sender === undefined) return ''
|
||||
const employee = await getEmployee(sender.person as Ref<Employee>, control)
|
||||
if (employee === undefined) return ''
|
||||
const who = formatName(employee.name)
|
||||
const who = formatName(employee.name, control.branding?.lastNameFirst)
|
||||
|
||||
const date = `on ${new Date(fromTzDate(holiday.date)).toLocaleDateString()}`
|
||||
|
||||
@ -417,7 +417,7 @@ export async function PublicHolidayTextPresenter (doc: Doc, control: TriggerCont
|
||||
if (sender === undefined) return ''
|
||||
const employee = await getEmployee(sender.person as Ref<Employee>, control)
|
||||
if (employee === undefined) return ''
|
||||
const who = formatName(employee.name)
|
||||
const who = formatName(employee.name, control.branding?.lastNameFirst)
|
||||
|
||||
const date = `on ${new Date(fromTzDate(holiday.date)).toLocaleDateString()}`
|
||||
|
||||
|
@ -25,7 +25,7 @@ import { workbenchId } from '@hcengineering/workbench'
|
||||
*/
|
||||
export async function productHTMLPresenter (doc: Doc, control: TriggerControl): Promise<string> {
|
||||
const product = doc as Product
|
||||
const front = getMetadata(serverCore.metadata.FrontUrl) ?? ''
|
||||
const front = control.branding?.front ?? getMetadata(serverCore.metadata.FrontUrl) ?? ''
|
||||
const path = `${workbenchId}/${control.workspace.workspaceUrl}/${inventoryId}/Products/#${view.component.EditDoc}|${product._id}|${product._class}|content`
|
||||
const link = concatLink(front, path)
|
||||
return `<a href="${link}">${product.name}</a>`
|
||||
|
@ -26,7 +26,7 @@ import { workbenchId } from '@hcengineering/workbench'
|
||||
*/
|
||||
export async function leadHTMLPresenter (doc: Doc, control: TriggerControl): Promise<string> {
|
||||
const lead = doc as Lead
|
||||
const front = getMetadata(serverCore.metadata.FrontUrl) ?? ''
|
||||
const front = control.branding?.front ?? getMetadata(serverCore.metadata.FrontUrl) ?? ''
|
||||
const path = `${workbenchId}/${control.workspace.workspaceUrl}/${leadId}/${lead.space}/#${view.component.EditDoc}|${lead._id}|${lead._class}|content`
|
||||
const link = concatLink(front, path)
|
||||
return `<a href="${link}">${lead.title}</a>`
|
||||
|
@ -82,7 +82,7 @@ async function createUserInfo (acc: Ref<Account>, control: TriggerControl): Prom
|
||||
const room = (await control.findAll(love.class.Office, { person: personId }))[0]
|
||||
const tx = control.txFactory.createTxCreateDoc(love.class.ParticipantInfo, love.space.Rooms, {
|
||||
person: personId,
|
||||
name: person !== undefined ? getName(control.hierarchy, person) : account.email,
|
||||
name: person !== undefined ? getName(control.hierarchy, person, control.branding?.lastNameFirst) : account.email,
|
||||
room: room?._id ?? love.ids.Reception,
|
||||
x: 0,
|
||||
y: 0
|
||||
@ -266,7 +266,9 @@ export async function OnKnock (tx: Tx, control: TriggerControl): Promise<Tx[]> {
|
||||
) {
|
||||
const path = [workbenchId, control.workspace.workspaceUrl, loveId]
|
||||
const title = await translate(love.string.KnockingLabel, {})
|
||||
const body = await translate(love.string.IsKnocking, { name: formatName(from.name) })
|
||||
const body = await translate(love.string.IsKnocking, {
|
||||
name: formatName(from.name, control.branding?.lastNameFirst)
|
||||
})
|
||||
await createPushNotification(control, userAcc._id, title, body, request._id, from, path)
|
||||
}
|
||||
}
|
||||
@ -294,7 +296,9 @@ export async function OnInvite (tx: Tx, control: TriggerControl): Promise<Tx[]>
|
||||
const title = await translate(love.string.InivitingLabel, {})
|
||||
const body =
|
||||
from !== undefined
|
||||
? await translate(love.string.InvitingYou, { name: formatName(from.name) })
|
||||
? await translate(love.string.InvitingYou, {
|
||||
name: formatName(from.name, control.branding?.lastNameFirst)
|
||||
})
|
||||
: await translate(love.string.InivitingLabel, {})
|
||||
await createPushNotification(control, userAcc._id, title, body, invite._id, from, path)
|
||||
}
|
||||
|
@ -229,7 +229,7 @@ async function notifyByEmail (
|
||||
|
||||
if (sender !== undefined) {
|
||||
const senderPerson = (await control.findAll(contact.class.Person, { _id: sender.person }))[0]
|
||||
senderName = senderPerson !== undefined ? formatName(senderPerson.name) : ''
|
||||
senderName = senderPerson !== undefined ? formatName(senderPerson.name, control.branding?.lastNameFirst) : ''
|
||||
}
|
||||
|
||||
const content = await getContent(doc, senderName, type, control, data)
|
||||
@ -611,7 +611,7 @@ export async function createPushNotification (
|
||||
if (_id !== undefined) {
|
||||
data.tag = _id
|
||||
}
|
||||
const front = getMetadata(serverCore.metadata.FrontUrl) ?? ''
|
||||
const front = control.branding?.front ?? getMetadata(serverCore.metadata.FrontUrl) ?? ''
|
||||
const uploadUrl = getMetadata(serverCore.metadata.UploadURL) ?? ''
|
||||
const domainPath = `${workbenchId}/${control.workspace.workspaceUrl}`
|
||||
const domain = concatLink(front, domainPath)
|
||||
|
@ -327,7 +327,7 @@ async function getFallbackNotificationFullfillment (
|
||||
if (account !== undefined) {
|
||||
const senderPerson = (cache.get(account.person) as Person) ?? (await findPersonForAccount(control, account.person))
|
||||
if (senderPerson !== undefined) {
|
||||
intlParams.senderName = formatName(senderPerson.name)
|
||||
intlParams.senderName = formatName(senderPerson.name, control.branding?.lastNameFirst)
|
||||
cache.set(senderPerson._id, senderPerson)
|
||||
}
|
||||
}
|
||||
|
@ -46,7 +46,7 @@ function getSequenceId (doc: Vacancy | Applicant, control: TriggerControl): stri
|
||||
*/
|
||||
export async function vacancyHTMLPresenter (doc: Doc, control: TriggerControl): Promise<string> {
|
||||
const vacancy = doc as Vacancy
|
||||
const front = getMetadata(serverCore.metadata.FrontUrl) ?? ''
|
||||
const front = control.branding?.front ?? getMetadata(serverCore.metadata.FrontUrl) ?? ''
|
||||
const path = `${workbenchId}/${control.workspace.workspaceUrl}/${recruitId}/${getSequenceId(vacancy, control)}`
|
||||
const link = concatLink(front, path)
|
||||
return `<a href="${link}">${vacancy.name}</a>`
|
||||
@ -65,7 +65,7 @@ export async function vacancyTextPresenter (doc: Doc): Promise<string> {
|
||||
*/
|
||||
export async function applicationHTMLPresenter (doc: Doc, control: TriggerControl): Promise<string> {
|
||||
const applicant = doc as Applicant
|
||||
const front = getMetadata(serverCore.metadata.FrontUrl) ?? ''
|
||||
const front = control.branding?.front ?? getMetadata(serverCore.metadata.FrontUrl) ?? ''
|
||||
const id = getSequenceId(applicant, control)
|
||||
const path = `${workbenchId}/${control.workspace.workspaceUrl}/${recruitId}/${id}`
|
||||
const link = concatLink(front, path)
|
||||
|
@ -59,7 +59,7 @@ async function updateSubIssues (
|
||||
*/
|
||||
export async function issueHTMLPresenter (doc: Doc, control: TriggerControl): Promise<string> {
|
||||
const issue = doc as Issue
|
||||
const front = getMetadata(serverCore.metadata.FrontUrl) ?? ''
|
||||
const front = control.branding?.front ?? getMetadata(serverCore.metadata.FrontUrl) ?? ''
|
||||
const path = `${workbenchId}/${control.workspace.workspaceUrl}/${trackerId}/${issue.identifier}`
|
||||
const link = concatLink(front, path)
|
||||
return `<a href="${link}">${issue.identifier}</a> ${issue.title}`
|
||||
|
@ -26,7 +26,7 @@ export const TrainingRequestHTMLPresenter: Presenter<TrainingRequest> = async (
|
||||
request: TrainingRequest,
|
||||
control: TriggerControl
|
||||
) => {
|
||||
const front = getMetadata(serverCore.metadata.FrontUrl) ?? ''
|
||||
const front = control.branding?.front ?? getMetadata(serverCore.metadata.FrontUrl) ?? ''
|
||||
// TODO: Don't hardcode URLs, find a way to share routes info between front and server resources, and DRY
|
||||
const path = `${workbenchId}/${control.workspace.workspaceUrl}/${trainingId}/requests/${request._id}`
|
||||
const link = concatLink(front, path)
|
||||
|
@ -7,14 +7,13 @@ import account, {
|
||||
UpgradeWorker,
|
||||
accountId,
|
||||
cleanInProgressWorkspaces,
|
||||
getMethods,
|
||||
type BrandingMap
|
||||
getMethods
|
||||
} from '@hcengineering/account'
|
||||
import accountEn from '@hcengineering/account/lang/en.json'
|
||||
import accountRu from '@hcengineering/account/lang/ru.json'
|
||||
import { Analytics } from '@hcengineering/analytics'
|
||||
import { registerProviders } from '@hcengineering/auth-providers'
|
||||
import { type Data, type MeasureContext, type Tx, type Version } from '@hcengineering/core'
|
||||
import { type Data, type MeasureContext, type Tx, type Version, type BrandingMap } from '@hcengineering/core'
|
||||
import { type MigrateOperation } from '@hcengineering/model'
|
||||
import platform, { Severity, Status, addStringsLoader, setMetadata } from '@hcengineering/platform'
|
||||
import serverToken from '@hcengineering/server-token'
|
||||
@ -101,7 +100,7 @@ export function serveAccount (
|
||||
|
||||
void client.then(async (p: MongoClient) => {
|
||||
const db = p.db(ACCOUNT_DB)
|
||||
registerProviders(measureCtx, app, router, db, productId, serverSecret, frontURL)
|
||||
registerProviders(measureCtx, app, router, db, productId, serverSecret, frontURL, brandings)
|
||||
|
||||
// We need to clean workspace with creating === true, since server is restarted.
|
||||
void cleanInProgressWorkspaces(db, productId)
|
||||
|
@ -42,7 +42,8 @@ import core, {
|
||||
TxOperations,
|
||||
Version,
|
||||
versionToString,
|
||||
WorkspaceId
|
||||
WorkspaceId,
|
||||
type Branding
|
||||
} from '@hcengineering/core'
|
||||
import { consoleModelLogger, MigrateOperation, ModelLogger } from '@hcengineering/model'
|
||||
import platform, { getMetadata, PlatformError, Severity, Status, translate } from '@hcengineering/platform'
|
||||
@ -513,7 +514,7 @@ async function sendConfirmation (productId: string, branding: Branding | null, a
|
||||
console.info('Please provide email service url to enable email confirmations.')
|
||||
return
|
||||
}
|
||||
const front = getMetadata(accountPlugin.metadata.FrontURL)
|
||||
const front = branding?.front ?? getMetadata(accountPlugin.metadata.FrontURL)
|
||||
if (front === undefined || front === '') {
|
||||
throw new Error('Please provide front url')
|
||||
}
|
||||
@ -806,10 +807,12 @@ async function generateWorkspaceRecord (
|
||||
email: string,
|
||||
productId: string,
|
||||
version: Data<Version>,
|
||||
branding: Branding | null,
|
||||
workspaceName: string,
|
||||
fixedWorkspace?: string
|
||||
): Promise<Workspace> {
|
||||
const coll = db.collection<Omit<Workspace, '_id'>>(WORKSPACE_COLLECTION)
|
||||
const brandingKey = branding?.key ?? 'huly'
|
||||
if (fixedWorkspace !== undefined) {
|
||||
const ws = await coll.find<Workspace>({ workspaceUrl: fixedWorkspace }).toArray()
|
||||
if ((await getWorkspaceById(db, productId, fixedWorkspace)) !== null || ws.length > 0) {
|
||||
@ -822,6 +825,7 @@ async function generateWorkspaceRecord (
|
||||
workspaceUrl: fixedWorkspace,
|
||||
productId,
|
||||
version,
|
||||
branding: brandingKey,
|
||||
workspaceName,
|
||||
accounts: [],
|
||||
disabled: true,
|
||||
@ -854,6 +858,7 @@ async function generateWorkspaceRecord (
|
||||
workspaceUrl,
|
||||
productId,
|
||||
version,
|
||||
branding: brandingKey,
|
||||
workspaceName,
|
||||
accounts: [],
|
||||
disabled: true,
|
||||
@ -909,7 +914,7 @@ export async function createWorkspace (
|
||||
await searchPromise
|
||||
|
||||
// Safe generate workspace record.
|
||||
searchPromise = generateWorkspaceRecord(db, email, productId, version, workspaceName, workspace)
|
||||
searchPromise = generateWorkspaceRecord(db, email, productId, version, branding, workspaceName, workspace)
|
||||
|
||||
const workspaceInfo = await searchPromise
|
||||
|
||||
@ -1670,7 +1675,7 @@ export async function requestPassword (
|
||||
if (sesURL === undefined || sesURL === '') {
|
||||
throw new Error('Please provide email service url')
|
||||
}
|
||||
const front = getMetadata(accountPlugin.metadata.FrontURL)
|
||||
const front = branding?.front ?? getMetadata(accountPlugin.metadata.FrontURL)
|
||||
if (front === undefined || front === '') {
|
||||
throw new Error('Please provide front url')
|
||||
}
|
||||
@ -1936,7 +1941,7 @@ export async function sendInvite (
|
||||
if (sesURL === undefined || sesURL === '') {
|
||||
throw new Error('Please provide email service url')
|
||||
}
|
||||
const front = getMetadata(accountPlugin.metadata.FrontURL)
|
||||
const front = branding?.front ?? getMetadata(accountPlugin.metadata.FrontURL)
|
||||
if (front === undefined || front === '') {
|
||||
throw new Error('Please provide front url')
|
||||
}
|
||||
@ -1995,14 +2000,6 @@ async function deactivatePersonAccount (
|
||||
}
|
||||
}
|
||||
|
||||
export interface Branding {
|
||||
title?: string
|
||||
language?: string
|
||||
initWorkspace?: string
|
||||
}
|
||||
|
||||
export type BrandingMap = Record<string, Branding>
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
|
@ -3,6 +3,7 @@ import core, {
|
||||
ModelDb,
|
||||
TxProcessor,
|
||||
toFindResult,
|
||||
type Branding,
|
||||
type Blob,
|
||||
type BlobLookup,
|
||||
type Class,
|
||||
@ -157,7 +158,12 @@ export class MemStorageAdapter implements StorageAdapter {
|
||||
throw new Error('NoSuchKey')
|
||||
}
|
||||
|
||||
async lookup (ctx: MeasureContext, workspaceId: WorkspaceIdWithUrl, docs: Blob[]): Promise<BlobLookupResult> {
|
||||
async lookup (
|
||||
ctx: MeasureContext,
|
||||
workspaceId: WorkspaceIdWithUrl,
|
||||
branding: Branding | null,
|
||||
docs: Blob[]
|
||||
): Promise<BlobLookupResult> {
|
||||
return { lookups: docs as unknown as BlobLookup[] }
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,8 @@ import core, {
|
||||
type MeasureContext,
|
||||
type Ref,
|
||||
type WorkspaceId,
|
||||
getFullTextContext
|
||||
getFullTextContext,
|
||||
type Branding
|
||||
} from '@hcengineering/core'
|
||||
import { jsonToText, markupToJSON } from '@hcengineering/text'
|
||||
import { type DbAdapter } from '../adapter'
|
||||
@ -66,7 +67,8 @@ export class FullTextPushStage implements FullTextPipelineStage {
|
||||
constructor (
|
||||
private readonly dbStorage: ServerStorage,
|
||||
readonly fulltextAdapter: FullTextAdapter,
|
||||
readonly workspace: WorkspaceId
|
||||
readonly workspace: WorkspaceId,
|
||||
readonly branding: Branding | null
|
||||
) {}
|
||||
|
||||
async initialize (ctx: MeasureContext, storage: DbAdapter, pipeline: FullTextPipeline): Promise<void> {
|
||||
@ -190,7 +192,7 @@ export class FullTextPushStage implements FullTextPipelineStage {
|
||||
})
|
||||
)
|
||||
|
||||
await updateDocWithPresenter(pipeline.hierarchy, doc, elasticDoc, { parentDoc, spaceDoc })
|
||||
await updateDocWithPresenter(pipeline.hierarchy, doc, elasticDoc, { parentDoc, spaceDoc }, this.branding)
|
||||
|
||||
this.checkIntegrity(elasticDoc)
|
||||
bulk.push(elasticDoc)
|
||||
|
@ -1,5 +1,6 @@
|
||||
import {
|
||||
docKey,
|
||||
type Branding,
|
||||
type Class,
|
||||
type Doc,
|
||||
type DocIndexState,
|
||||
@ -110,7 +111,8 @@ export async function updateDocWithPresenter (
|
||||
refDocs: {
|
||||
parentDoc: DocIndexState | undefined
|
||||
spaceDoc: DocIndexState | undefined
|
||||
}
|
||||
},
|
||||
branding: Branding | null
|
||||
): Promise<void> {
|
||||
const searchPresenter = findSearchPresenter(hierarchy, doc.objectClass)
|
||||
if (searchPresenter === undefined) {
|
||||
@ -134,7 +136,8 @@ export async function updateDocWithPresenter (
|
||||
props.push({
|
||||
name: 'searchShortTitle',
|
||||
config: searchPresenter.searchConfig.shortTitle,
|
||||
provider: searchPresenter.getSearchShortTitle
|
||||
provider: searchPresenter.getSearchShortTitle,
|
||||
lastNameFirst: branding?.lastNameFirst
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -28,7 +28,8 @@ import {
|
||||
type SearchResult,
|
||||
type StorageIterator,
|
||||
type Tx,
|
||||
type TxResult
|
||||
type TxResult,
|
||||
type Branding
|
||||
} from '@hcengineering/core'
|
||||
import { type DbConfiguration } from './configuration'
|
||||
import { createServerStorage } from './server'
|
||||
@ -49,7 +50,8 @@ export async function createPipeline (
|
||||
conf: DbConfiguration,
|
||||
constructors: MiddlewareCreator[],
|
||||
upgrade: boolean,
|
||||
broadcast: BroadcastFunc
|
||||
broadcast: BroadcastFunc,
|
||||
branding: Branding | null
|
||||
): Promise<Pipeline> {
|
||||
const broadcastHandlers: BroadcastFunc[] = [broadcast]
|
||||
const _broadcast: BroadcastFunc = (
|
||||
@ -65,7 +67,8 @@ export async function createPipeline (
|
||||
async (ctx) =>
|
||||
await createServerStorage(ctx, conf, {
|
||||
upgrade,
|
||||
broadcast: _broadcast
|
||||
broadcast: _broadcast,
|
||||
branding
|
||||
})
|
||||
)
|
||||
const pipelineResult = await PipelineImpl.create(ctx.newChild('pipeline-operations', {}), storage, constructors)
|
||||
|
@ -6,7 +6,8 @@ import core, {
|
||||
type MeasureContext,
|
||||
type Ref,
|
||||
type WorkspaceId,
|
||||
type WorkspaceIdWithUrl
|
||||
type WorkspaceIdWithUrl,
|
||||
type Branding
|
||||
} from '@hcengineering/core'
|
||||
import { type Readable } from 'stream'
|
||||
import { type RawDBAdapter } from '../adapter'
|
||||
@ -292,14 +293,19 @@ export class AggregatorStorageAdapter implements StorageAdapter, StorageAdapterE
|
||||
return result
|
||||
}
|
||||
|
||||
async lookup (ctx: MeasureContext, workspaceId: WorkspaceIdWithUrl, docs: Blob[]): Promise<BlobLookupResult> {
|
||||
async lookup (
|
||||
ctx: MeasureContext,
|
||||
workspaceId: WorkspaceIdWithUrl,
|
||||
branding: Branding | null,
|
||||
docs: Blob[]
|
||||
): Promise<BlobLookupResult> {
|
||||
const result: BlobLookup[] = []
|
||||
|
||||
const byProvider = groupByArray(docs, (it) => it.provider)
|
||||
for (const [k, v] of byProvider.entries()) {
|
||||
const provider = this.adapters.get(k)
|
||||
if (provider?.lookup !== undefined) {
|
||||
const upd = await provider.lookup(ctx, workspaceId, v)
|
||||
const upd = await provider.lookup(ctx, workspaceId, branding, v)
|
||||
if (upd.updates !== undefined) {
|
||||
await this.dbAdapter.update(ctx, workspaceId, DOMAIN_BLOB, upd.updates)
|
||||
}
|
||||
|
@ -709,6 +709,7 @@ export class TServerStorage implements ServerStorage {
|
||||
operationContext: ctx,
|
||||
removedMap,
|
||||
workspace: this.workspaceId,
|
||||
branding: this.options.branding,
|
||||
storageAdapter: this.storageAdapter,
|
||||
serviceAdaptersManager: this.serviceAdaptersManager,
|
||||
findAll: fAll(ctx.ctx),
|
||||
@ -746,7 +747,8 @@ export class TServerStorage implements ServerStorage {
|
||||
sctx.sessionId,
|
||||
sctx.admin,
|
||||
[],
|
||||
this.workspaceId
|
||||
this.workspaceId,
|
||||
this.options.branding
|
||||
)
|
||||
const result = await performAsync(applyCtx)
|
||||
// We need to broadcast changes
|
||||
|
@ -40,7 +40,8 @@ import {
|
||||
type TxFactory,
|
||||
type TxResult,
|
||||
type WorkspaceId,
|
||||
type WorkspaceIdWithUrl
|
||||
type WorkspaceIdWithUrl,
|
||||
type Branding
|
||||
} from '@hcengineering/core'
|
||||
import type { Asset, Resource } from '@hcengineering/platform'
|
||||
import { type Readable } from 'stream'
|
||||
@ -68,7 +69,7 @@ export interface ServerStorage extends LowLevelStorage {
|
||||
close: () => Promise<void>
|
||||
loadModel: (last: Timestamp, hash?: string) => Promise<Tx[] | LoadModelResponse>
|
||||
workspaceId: WorkspaceIdWithUrl
|
||||
|
||||
branding?: string
|
||||
storageAdapter: StorageAdapter
|
||||
}
|
||||
|
||||
@ -81,6 +82,7 @@ export interface SessionContext extends SessionOperationContext {
|
||||
admin?: boolean
|
||||
|
||||
workspace: WorkspaceIdWithUrl
|
||||
branding: Branding | null
|
||||
}
|
||||
|
||||
/**
|
||||
@ -142,6 +144,7 @@ export interface TriggerControl {
|
||||
operationContext: SessionOperationContext
|
||||
ctx: MeasureContext
|
||||
workspace: WorkspaceIdWithUrl
|
||||
branding: Branding | null
|
||||
txFactory: TxFactory
|
||||
findAll: Storage['findAll']
|
||||
findAllCtx: <T extends Doc>(
|
||||
@ -438,6 +441,7 @@ export interface ServerStorageOptions {
|
||||
upgrade: boolean
|
||||
|
||||
broadcast: BroadcastFunc
|
||||
branding: Branding | null
|
||||
}
|
||||
|
||||
export interface ServiceAdapter {
|
||||
|
@ -10,9 +10,12 @@ import core, {
|
||||
type ParamsType,
|
||||
type Ref,
|
||||
type TxWorkspaceEvent,
|
||||
type WorkspaceIdWithUrl
|
||||
type WorkspaceIdWithUrl,
|
||||
type Branding,
|
||||
type BrandingMap
|
||||
} from '@hcengineering/core'
|
||||
import { type Hash } from 'crypto'
|
||||
import fs from 'fs'
|
||||
import type { SessionContext } from './types'
|
||||
|
||||
/**
|
||||
@ -138,7 +141,8 @@ export class SessionContextImpl implements SessionContext {
|
||||
readonly sessionId: string,
|
||||
readonly admin: boolean | undefined,
|
||||
readonly derived: SessionContext['derived'],
|
||||
readonly workspace: WorkspaceIdWithUrl
|
||||
readonly workspace: WorkspaceIdWithUrl,
|
||||
readonly branding: Branding | null
|
||||
) {}
|
||||
|
||||
with<T>(
|
||||
@ -151,7 +155,17 @@ export class SessionContextImpl implements SessionContext {
|
||||
name,
|
||||
params,
|
||||
async (ctx) =>
|
||||
await op(new SessionContextImpl(ctx, this.userEmail, this.sessionId, this.admin, this.derived, this.workspace)),
|
||||
await op(
|
||||
new SessionContextImpl(
|
||||
ctx,
|
||||
this.userEmail,
|
||||
this.sessionId,
|
||||
this.admin,
|
||||
this.derived,
|
||||
this.workspace,
|
||||
this.branding
|
||||
)
|
||||
),
|
||||
fullParams
|
||||
)
|
||||
}
|
||||
@ -171,3 +185,16 @@ export function createBroadcastEvent (classes: Ref<Class<Doc>>[]): TxWorkspaceEv
|
||||
space: core.space.DerivedTx
|
||||
}
|
||||
}
|
||||
|
||||
export function loadBrandingMap (brandingPath?: string): BrandingMap {
|
||||
let brandings: BrandingMap = {}
|
||||
if (brandingPath !== undefined && brandingPath !== '') {
|
||||
brandings = JSON.parse(fs.readFileSync(brandingPath, 'utf8'))
|
||||
|
||||
for (const [host, value] of Object.entries(brandings)) {
|
||||
value.front = `https://${host}/`
|
||||
}
|
||||
}
|
||||
|
||||
return brandings
|
||||
}
|
||||
|
@ -26,7 +26,8 @@ import core, {
|
||||
toIdMap,
|
||||
type Blob,
|
||||
type BlobLookup,
|
||||
type WorkspaceIdWithUrl
|
||||
type WorkspaceIdWithUrl,
|
||||
type Branding
|
||||
} from '@hcengineering/core'
|
||||
import { Middleware, SessionContext, TxMiddlewareResult, type ServerStorage } from '@hcengineering/server-core'
|
||||
import { BaseMiddleware } from './base'
|
||||
@ -50,12 +51,13 @@ export class BlobLookupMiddleware extends BaseMiddleware implements Middleware {
|
||||
async fetchBlobInfo (
|
||||
ctx: MeasureContext,
|
||||
workspace: WorkspaceIdWithUrl,
|
||||
branding: Branding | null,
|
||||
toUpdate: [Doc, Blob, string][]
|
||||
): Promise<void> {
|
||||
if (this.storage.storageAdapter.lookup !== undefined) {
|
||||
const docsToUpdate = toUpdate.map((it) => it[1])
|
||||
const updatedBlobs = toIdMap<Blob>(
|
||||
(await this.storage.storageAdapter.lookup(ctx, workspace, docsToUpdate)).lookups
|
||||
(await this.storage.storageAdapter.lookup(ctx, workspace, branding, docsToUpdate)).lookups
|
||||
)
|
||||
for (const [doc, blob, key] of toUpdate) {
|
||||
const ublob = updatedBlobs.get(blob._id)
|
||||
@ -78,7 +80,8 @@ export class BlobLookupMiddleware extends BaseMiddleware implements Middleware {
|
||||
if (_class === core.class.Blob) {
|
||||
// Bulk update of info
|
||||
const updatedBlobs = toIdMap<Blob>(
|
||||
(await this.storage.storageAdapter.lookup(ctx.ctx, ctx.workspace, result as unknown as Blob[])).lookups
|
||||
(await this.storage.storageAdapter.lookup(ctx.ctx, ctx.workspace, ctx.branding, result as unknown as Blob[]))
|
||||
.lookups
|
||||
)
|
||||
const res: T[] = []
|
||||
for (const d of result) {
|
||||
@ -102,13 +105,13 @@ export class BlobLookupMiddleware extends BaseMiddleware implements Middleware {
|
||||
}
|
||||
if (toUpdate.length > 50) {
|
||||
// Bulk update of info
|
||||
await this.fetchBlobInfo(ctx.ctx, ctx.workspace, toUpdate)
|
||||
await this.fetchBlobInfo(ctx.ctx, ctx.workspace, ctx.branding, toUpdate)
|
||||
toUpdate = []
|
||||
}
|
||||
}
|
||||
if (toUpdate.length > 0) {
|
||||
// Bulk update of info
|
||||
await this.fetchBlobInfo(ctx.ctx, ctx.workspace, toUpdate)
|
||||
await this.fetchBlobInfo(ctx.ctx, ctx.workspace, ctx.branding, toUpdate)
|
||||
toUpdate = []
|
||||
}
|
||||
}
|
||||
|
@ -24,7 +24,8 @@ import core, {
|
||||
type MeasureContext,
|
||||
type Ref,
|
||||
type WorkspaceId,
|
||||
type WorkspaceIdWithUrl
|
||||
type WorkspaceIdWithUrl,
|
||||
type Branding
|
||||
} from '@hcengineering/core'
|
||||
|
||||
import { getMetadata } from '@hcengineering/platform'
|
||||
@ -76,8 +77,13 @@ export class MinioService implements StorageAdapter {
|
||||
|
||||
async initialize (ctx: MeasureContext, workspaceId: WorkspaceId): Promise<void> {}
|
||||
|
||||
async lookup (ctx: MeasureContext, workspaceId: WorkspaceIdWithUrl, docs: Blob[]): Promise<BlobLookupResult> {
|
||||
const frontUrl = getMetadata(serverCore.metadata.FrontUrl) ?? ''
|
||||
async lookup (
|
||||
ctx: MeasureContext,
|
||||
workspaceId: WorkspaceIdWithUrl,
|
||||
branding: Branding | null,
|
||||
docs: Blob[]
|
||||
): Promise<BlobLookupResult> {
|
||||
const frontUrl = branding?.front ?? getMetadata(serverCore.metadata.FrontUrl) ?? ''
|
||||
for (const d of docs) {
|
||||
// Let's add current from URI for previews.
|
||||
const bl = d as BlobLookup
|
||||
|
@ -177,7 +177,8 @@ describe('mongo operations', () => {
|
||||
const ctx = new MeasureMetricsContext('client', {})
|
||||
serverStorage = await createServerStorage(ctx, conf, {
|
||||
upgrade: false,
|
||||
broadcast: () => {}
|
||||
broadcast: () => {},
|
||||
branding: null
|
||||
})
|
||||
const soCtx: SessionOperationContext = {
|
||||
ctx,
|
||||
|
@ -25,7 +25,8 @@ import core, {
|
||||
type MeasureContext,
|
||||
type Ref,
|
||||
type WorkspaceId,
|
||||
type WorkspaceIdWithUrl
|
||||
type WorkspaceIdWithUrl,
|
||||
type Branding
|
||||
} from '@hcengineering/core'
|
||||
|
||||
import {
|
||||
@ -83,7 +84,12 @@ export class S3Service implements StorageAdapter {
|
||||
|
||||
async initialize (ctx: MeasureContext, workspaceId: WorkspaceId): Promise<void> {}
|
||||
|
||||
async lookup (ctx: MeasureContext, workspaceId: WorkspaceIdWithUrl, docs: Blob[]): Promise<BlobLookupResult> {
|
||||
async lookup (
|
||||
ctx: MeasureContext,
|
||||
workspaceId: WorkspaceIdWithUrl,
|
||||
branding: Branding | null,
|
||||
docs: Blob[]
|
||||
): Promise<BlobLookupResult> {
|
||||
const result: BlobLookupResult = {
|
||||
lookups: [],
|
||||
updates: new Map()
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Doc, DocInfo, Domain, Ref, StorageIterator } from '@hcengineering/core'
|
||||
import { type BrandingMap, Doc, DocInfo, Domain, Ref, StorageIterator } from '@hcengineering/core'
|
||||
import { Pipeline, estimateDocSize } from '@hcengineering/server-core'
|
||||
import { Token } from '@hcengineering/server-token'
|
||||
import { ClientSession, Session, type ClientSessionCtx } from '@hcengineering/server-ws'
|
||||
@ -30,9 +30,10 @@ export interface BackupSession extends Session {
|
||||
export class BackupClientSession extends ClientSession implements BackupSession {
|
||||
constructor (
|
||||
protected readonly token: Token,
|
||||
protected readonly _pipeline: Pipeline
|
||||
protected readonly _pipeline: Pipeline,
|
||||
protected readonly brandingMap: BrandingMap
|
||||
) {
|
||||
super(token, _pipeline)
|
||||
super(token, _pipeline, brandingMap)
|
||||
}
|
||||
|
||||
idIndex = 0
|
||||
|
@ -13,6 +13,7 @@ export interface ServerEnv {
|
||||
pushPublicKey: string | undefined
|
||||
pushPrivateKey: string | undefined
|
||||
pushSubject: string | undefined
|
||||
brandingPath: string | undefined
|
||||
}
|
||||
|
||||
export function serverConfigFromEnv (): ServerEnv {
|
||||
@ -71,6 +72,8 @@ export function serverConfigFromEnv (): ServerEnv {
|
||||
const pushPublicKey = process.env.PUSH_PUBLIC_KEY
|
||||
const pushPrivateKey = process.env.PUSH_PRIVATE_KEY
|
||||
const pushSubject = process.env.PUSH_SUBJECT
|
||||
const brandingPath = process.env.BRANDING_PATH
|
||||
|
||||
return {
|
||||
url,
|
||||
elasticUrl,
|
||||
@ -85,6 +88,7 @@ export function serverConfigFromEnv (): ServerEnv {
|
||||
enableCompression,
|
||||
pushPublicKey,
|
||||
pushPrivateKey,
|
||||
pushSubject
|
||||
pushSubject,
|
||||
brandingPath
|
||||
}
|
||||
}
|
||||
|
@ -82,9 +82,10 @@ describe('server', () => {
|
||||
return { docs: [] }
|
||||
}
|
||||
}),
|
||||
sessionFactory: (token, pipeline) => new ClientSession(token, pipeline),
|
||||
sessionFactory: (token, pipeline) => new ClientSession(token, pipeline, {}),
|
||||
port: 3335,
|
||||
productId: '',
|
||||
brandingMap: {},
|
||||
serverFactory: startHttpServer,
|
||||
accountsUrl: '',
|
||||
externalStorage: createDummyStorageAdapter()
|
||||
@ -182,9 +183,10 @@ describe('server', () => {
|
||||
return { docs: [] }
|
||||
}
|
||||
}),
|
||||
sessionFactory: (token, pipeline) => new ClientSession(token, pipeline),
|
||||
sessionFactory: (token, pipeline) => new ClientSession(token, pipeline, {}),
|
||||
port: 3336,
|
||||
productId: '',
|
||||
brandingMap: {},
|
||||
serverFactory: startHttpServer,
|
||||
accountsUrl: '',
|
||||
externalStorage: createDummyStorageAdapter()
|
||||
|
@ -32,7 +32,9 @@ import core, {
|
||||
type Tx,
|
||||
type TxApplyIf,
|
||||
type TxApplyResult,
|
||||
type TxCUD
|
||||
type TxCUD,
|
||||
type Branding,
|
||||
type BrandingMap
|
||||
} from '@hcengineering/core'
|
||||
import { SessionContextImpl, createBroadcastEvent, type Pipeline } from '@hcengineering/server-core'
|
||||
import { type Token } from '@hcengineering/server-token'
|
||||
@ -56,7 +58,8 @@ export class ClientSession implements Session {
|
||||
|
||||
constructor (
|
||||
protected readonly token: Token,
|
||||
protected readonly _pipeline: Pipeline
|
||||
protected readonly _pipeline: Pipeline,
|
||||
protected readonly brandingMap: BrandingMap
|
||||
) {}
|
||||
|
||||
getUser (): string {
|
||||
@ -75,6 +78,14 @@ export class ClientSession implements Session {
|
||||
return this._pipeline
|
||||
}
|
||||
|
||||
getBranding (brandingKey?: string): Branding | null {
|
||||
if (brandingKey === undefined) {
|
||||
return null
|
||||
}
|
||||
|
||||
return this.brandingMap[brandingKey] ?? null
|
||||
}
|
||||
|
||||
async ping (ctx: ClientSessionCtx): Promise<void> {
|
||||
// console.log('ping')
|
||||
this.lastRequest = Date.now()
|
||||
@ -115,7 +126,8 @@ export class ClientSession implements Session {
|
||||
this.sessionId,
|
||||
this.token.extra?.admin === 'true',
|
||||
[],
|
||||
this._pipeline.storage.workspaceId
|
||||
this._pipeline.storage.workspaceId,
|
||||
this.getBranding(this._pipeline.storage.branding)
|
||||
)
|
||||
await this._pipeline.tx(context, createTx)
|
||||
const acc = TxProcessor.createDoc2Doc(createTx)
|
||||
@ -144,7 +156,8 @@ export class ClientSession implements Session {
|
||||
this.sessionId,
|
||||
this.token.extra?.admin === 'true',
|
||||
[],
|
||||
this._pipeline.storage.workspaceId
|
||||
this._pipeline.storage.workspaceId,
|
||||
this.getBranding(this._pipeline.storage.branding)
|
||||
)
|
||||
return await this._pipeline.findAll(context, _class, query, options)
|
||||
}
|
||||
@ -166,7 +179,8 @@ export class ClientSession implements Session {
|
||||
this.sessionId,
|
||||
this.token.extra?.admin === 'true',
|
||||
[],
|
||||
this._pipeline.storage.workspaceId
|
||||
this._pipeline.storage.workspaceId,
|
||||
this.getBranding(this._pipeline.storage.branding)
|
||||
)
|
||||
await ctx.sendResponse(await this._pipeline.searchFulltext(context, query, options))
|
||||
}
|
||||
@ -181,7 +195,8 @@ export class ClientSession implements Session {
|
||||
this.sessionId,
|
||||
this.token.extra?.admin === 'true',
|
||||
[],
|
||||
this._pipeline.storage.workspaceId
|
||||
this._pipeline.storage.workspaceId,
|
||||
this.getBranding(this._pipeline.storage.branding)
|
||||
)
|
||||
|
||||
const result = await this._pipeline.tx(context, tx)
|
||||
|
@ -26,7 +26,9 @@ import core, {
|
||||
type MeasureContext,
|
||||
type Tx,
|
||||
type TxWorkspaceEvent,
|
||||
type WorkspaceId
|
||||
type WorkspaceId,
|
||||
type Branding,
|
||||
type BrandingMap
|
||||
} from '@hcengineering/core'
|
||||
import { unknownError, type Status } from '@hcengineering/platform'
|
||||
import { type HelloRequest, type HelloResponse, type Request, type Response } from '@hcengineering/rpc'
|
||||
@ -92,7 +94,8 @@ class TSessionManager implements SessionManager {
|
||||
constructor (
|
||||
readonly ctx: MeasureContext,
|
||||
readonly sessionFactory: (token: Token, pipeline: Pipeline) => Session,
|
||||
readonly timeouts: Timeouts
|
||||
readonly timeouts: Timeouts,
|
||||
readonly brandingMap: BrandingMap
|
||||
) {
|
||||
this.checkInterval = setInterval(() => {
|
||||
this.handleInterval()
|
||||
@ -297,6 +300,10 @@ class TSessionManager implements SessionManager {
|
||||
await this.close(ctx, oldSession.socket, wsString)
|
||||
}
|
||||
const workspaceName = workspaceInfo.workspaceName ?? workspaceInfo.workspaceUrl ?? workspaceInfo.workspaceId
|
||||
const branding =
|
||||
(workspaceInfo.branding !== undefined
|
||||
? Object.values(this.brandingMap).find((b) => b.key === workspaceInfo.branding)
|
||||
: null) ?? null
|
||||
|
||||
if (workspace === undefined) {
|
||||
ctx.warn('open workspace', {
|
||||
@ -310,7 +317,8 @@ class TSessionManager implements SessionManager {
|
||||
pipelineFactory,
|
||||
token,
|
||||
workspaceInfo.workspaceUrl ?? workspaceInfo.workspaceId,
|
||||
workspaceName
|
||||
workspaceName,
|
||||
branding
|
||||
)
|
||||
}
|
||||
|
||||
@ -424,7 +432,8 @@ class TSessionManager implements SessionManager {
|
||||
true,
|
||||
(tx, targets, exclude) => {
|
||||
this.broadcastAll(workspace, tx, targets, exclude)
|
||||
}
|
||||
},
|
||||
workspace.branding
|
||||
)
|
||||
return await workspace.pipeline
|
||||
}
|
||||
@ -513,7 +522,8 @@ class TSessionManager implements SessionManager {
|
||||
pipelineFactory: PipelineFactory,
|
||||
token: Token,
|
||||
workspaceUrl: string,
|
||||
workspaceName: string
|
||||
workspaceName: string,
|
||||
branding: Branding | null
|
||||
): Workspace {
|
||||
const upgrade = token.extra?.model === 'upgrade'
|
||||
const backup = token.extra?.mode === 'backup'
|
||||
@ -528,14 +538,16 @@ class TSessionManager implements SessionManager {
|
||||
upgrade,
|
||||
(tx, targets) => {
|
||||
this.broadcastAll(workspace, tx, targets)
|
||||
}
|
||||
},
|
||||
branding
|
||||
),
|
||||
sessions: new Map(),
|
||||
softShutdown: 3,
|
||||
upgrade,
|
||||
backup,
|
||||
workspaceId: token.workspace,
|
||||
workspaceName
|
||||
workspaceName,
|
||||
branding
|
||||
}
|
||||
this.workspaces.set(toWorkspaceString(token.workspace), workspace)
|
||||
return workspace
|
||||
@ -970,16 +982,22 @@ export function start (
|
||||
pipelineFactory: PipelineFactory
|
||||
sessionFactory: (token: Token, pipeline: Pipeline) => Session
|
||||
productId: string
|
||||
brandingMap: BrandingMap
|
||||
serverFactory: ServerFactory
|
||||
enableCompression?: boolean
|
||||
accountsUrl: string
|
||||
externalStorage: StorageAdapter
|
||||
} & Partial<Timeouts>
|
||||
): () => Promise<void> {
|
||||
const sessions = new TSessionManager(ctx, opt.sessionFactory, {
|
||||
const sessions = new TSessionManager(
|
||||
ctx,
|
||||
opt.sessionFactory,
|
||||
{
|
||||
pingTimeout: opt.pingTimeout ?? 10000,
|
||||
reconnectTimeout: 500
|
||||
})
|
||||
},
|
||||
opt.brandingMap
|
||||
)
|
||||
return opt.serverFactory(
|
||||
sessions,
|
||||
(rctx, service, ws, msg, workspace) => {
|
||||
|
@ -8,7 +8,8 @@ import {
|
||||
type Ref,
|
||||
type Tx,
|
||||
type WorkspaceId,
|
||||
type WorkspaceIdWithUrl
|
||||
type WorkspaceIdWithUrl,
|
||||
type Branding
|
||||
} from '@hcengineering/core'
|
||||
import { type Request, type Response } from '@hcengineering/rpc'
|
||||
import { type BroadcastFunc, type Pipeline, type StorageAdapter } from '@hcengineering/server-core'
|
||||
@ -92,7 +93,8 @@ export type PipelineFactory = (
|
||||
ctx: MeasureContext,
|
||||
ws: WorkspaceIdWithUrl,
|
||||
upgrade: boolean,
|
||||
broadcast: BroadcastFunc
|
||||
broadcast: BroadcastFunc,
|
||||
branding: Branding | null
|
||||
) => Promise<Pipeline>
|
||||
|
||||
/**
|
||||
@ -134,6 +136,7 @@ export interface Workspace {
|
||||
|
||||
workspaceId: WorkspaceId
|
||||
workspaceName: string
|
||||
branding: Branding | null
|
||||
}
|
||||
|
||||
export interface AddSessionActive {
|
||||
|
@ -39,6 +39,8 @@ services:
|
||||
- minio
|
||||
ports:
|
||||
- 3003:3003
|
||||
volumes:
|
||||
- ./branding-test.json:/var/cfg/branding.json
|
||||
environment:
|
||||
- ACCOUNT_PORT=3003
|
||||
- SERVER_SECRET=secret
|
||||
@ -47,6 +49,7 @@ services:
|
||||
- ENDPOINT_URL=ws://localhost:3334
|
||||
- STORAGE_CONFIG=${STORAGE_CONFIG}
|
||||
- MODEL_ENABLED=*
|
||||
- BRANDING_PATH=/var/cfg/branding.json
|
||||
front:
|
||||
image: hardcoreeng/front
|
||||
pull_policy: never
|
||||
@ -87,6 +90,8 @@ services:
|
||||
- account
|
||||
ports:
|
||||
- 3334:3334
|
||||
volumes:
|
||||
- ./branding-test.json:/var/cfg/branding.json
|
||||
environment:
|
||||
- SERVER_PROVIDER=${SERVER_PROVIDER}
|
||||
- SERVER_PORT=3334
|
||||
@ -102,6 +107,7 @@ services:
|
||||
- ACCOUNTS_URL=http://account:3003
|
||||
- LAST_NAME_FIRST=true
|
||||
- ELASTIC_INDEX_NAME=local_storage_index
|
||||
- BRANDING_PATH=/var/cfg/branding.json
|
||||
collaborator:
|
||||
image: hardcoreeng/collaborator
|
||||
links:
|
||||
|
Loading…
Reference in New Issue
Block a user