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