diff --git a/dev/docker-compose.yaml b/dev/docker-compose.yaml
index e1a4a6affd..c2f6254e2d 100644
--- a/dev/docker-compose.yaml
+++ b/dev/docker-compose.yaml
@@ -85,9 +85,6 @@ services:
- COLLABORATOR_URL=ws://localhost:3078
- COLLABORATOR_API_URL=http://localhost:3078
- STORAGE_CONFIG=${STORAGE_CONFIG}
- - TITLE=DevPlatform
- - DEFAULT_LANGUAGE=ru
- - LAST_NAME_FIRST=true
restart: unless-stopped
collaborator:
image: hardcoreeng/collaborator
diff --git a/dev/prod/public/branding.json b/dev/prod/public/branding.json
new file mode 100644
index 0000000000..9108755de3
--- /dev/null
+++ b/dev/prod/public/branding.json
@@ -0,0 +1,58 @@
+{
+ "localhost:8080": {
+ "title": "Platform",
+ "languages": "en,ru,pt,es",
+ "defaultLanguage": "en",
+ "defaultApplication": "tracker",
+ "defaultSpace": "tracker:project:DefaultProject",
+ "defaultSpecial": "issues",
+ "links": [
+ {
+ "rel": "manifest",
+ "href": "/platform/site.webmanifest"
+ },
+ {
+ "rel": "icon",
+ "href": "/platform/favicon.svg",
+ "type": "image/svg+xml"
+ },
+ {
+ "rel": "shortcut icon",
+ "href": "/platform/favicon.ico",
+ "sizes": "any"
+ },
+ {
+ "rel": "apple-touch-icon",
+ "href": "/platform/icon-192.png"
+ }
+ ]
+ },
+ "localhost:8087": {
+ "title": "DevPlatform",
+ "languages": "en,ru,pt,es",
+ "defaultLanguage": "en",
+ "defaultApplication": "tracker",
+ "defaultSpace": "tracker:project:DefaultProject",
+ "defaultSpecial": "issues",
+ "links": [
+ {
+ "rel": "manifest",
+ "href": "/platform/site.webmanifest"
+ },
+ {
+ "rel": "icon",
+ "href": "/platform/favicon.svg",
+ "type": "image/svg+xml"
+ },
+ {
+ "rel": "shortcut icon",
+ "href": "/platform/favicon.ico",
+ "sizes": "any"
+ },
+ {
+ "rel": "apple-touch-icon",
+ "href": "/platform/icon-192.png"
+ }
+ ]
+ }
+}
diff --git a/dev/prod/public/config.json b/dev/prod/public/config.json
index 22b22e1e3f..96df6d0db1 100644
--- a/dev/prod/public/config.json
+++ b/dev/prod/public/config.json
@@ -7,6 +7,5 @@
"CALENDAR_URL": "http://localhost:8095",
"REKONI_URL": "http://localhost:4004",
"COLLABORATOR_URL": "ws://localhost:3078",
- "COLLABORATOR_API_URL": "http://localhost:3078",
- "LAST_NAME_FIRST": "true"
+ "COLLABORATOR_API_URL": "http://localhost:3078"
}
\ No newline at end of file
diff --git a/dev/prod/public/favicon.ico b/dev/prod/public/platform/favicon.ico
similarity index 100%
rename from dev/prod/public/favicon.ico
rename to dev/prod/public/platform/favicon.ico
diff --git a/dev/prod/public/favicon.svg b/dev/prod/public/platform/favicon.svg
similarity index 100%
rename from dev/prod/public/favicon.svg
rename to dev/prod/public/platform/favicon.svg
diff --git a/dev/prod/public/icon-1024.png b/dev/prod/public/platform/icon-1024.png
similarity index 100%
rename from dev/prod/public/icon-1024.png
rename to dev/prod/public/platform/icon-1024.png
diff --git a/dev/prod/public/icon-1600.png b/dev/prod/public/platform/icon-1600.png
similarity index 100%
rename from dev/prod/public/icon-1600.png
rename to dev/prod/public/platform/icon-1600.png
diff --git a/dev/prod/public/icon-192.png b/dev/prod/public/platform/icon-192.png
similarity index 100%
rename from dev/prod/public/icon-192.png
rename to dev/prod/public/platform/icon-192.png
diff --git a/dev/prod/public/icon-256.png b/dev/prod/public/platform/icon-256.png
similarity index 100%
rename from dev/prod/public/icon-256.png
rename to dev/prod/public/platform/icon-256.png
diff --git a/dev/prod/public/icon-512.png b/dev/prod/public/platform/icon-512.png
similarity index 100%
rename from dev/prod/public/icon-512.png
rename to dev/prod/public/platform/icon-512.png
diff --git a/dev/prod/public/site.webmanifest b/dev/prod/public/platform/site.webmanifest
similarity index 100%
rename from dev/prod/public/site.webmanifest
rename to dev/prod/public/platform/site.webmanifest
diff --git a/dev/prod/src/index.ejs b/dev/prod/src/index.ejs
index 8713736b58..1c1d8aa1b7 100644
--- a/dev/prod/src/index.ejs
+++ b/dev/prod/src/index.ejs
@@ -3,13 +3,8 @@
-
Platform
-
-
-
-
-
+
diff --git a/dev/prod/src/platform.ts b/dev/prod/src/platform.ts
index 23d2cf6e75..3e43b85ec2 100644
--- a/dev/prod/src/platform.ts
+++ b/dev/prod/src/platform.ts
@@ -101,12 +101,28 @@ interface Config {
COLLABORATOR_URL: string
COLLABORATOR_API_URL: string
PUSH_PUBLIC_KEY: string
- TITLE?: string
- LANGUAGES?: string
- DEFAULT_LANGUAGE?: string
- LAST_NAME_FIRST?: string
+ BRANDING_URL?: string
}
+export interface Branding {
+ title?: string
+ links?: {
+ rel: string
+ href: string
+ type?: string
+ sizes?: string
+ }[]
+ languages?: string
+ lastNameFirst?: string
+ defaultLanguage?: string
+ defaultApplication?: string
+ defaultSpace?: string
+ defaultSpecial?: string
+ initWorkspace?: string
+}
+
+export type BrandingMap = Record
+
const devConfig = process.env.CLIENT_TYPE === 'dev-production'
function configureI18n(): void {
@@ -164,7 +180,40 @@ export async function configurePlatform() {
configureI18n()
const config: Config = await (await fetch(devConfig? '/config-dev.json' : '/config.json')).json()
+ const branding: BrandingMap = await (await fetch(config.BRANDING_URL ?? '/branding.json')).json()
+ const myBranding = branding[window.location.host] ?? {}
+
console.log('loading configuration', config)
+ console.log('loaded branding', myBranding)
+
+ const title = myBranding.title ?? 'Platform'
+
+ // apply branding
+ window.document.title = title
+
+ const links = myBranding.links ?? []
+ if (links.length > 0) {
+ // remove the default favicon
+ // it's only needed for Safari which cannot use dynamically added links for favicons
+ document.getElementById('default-favicon')?.remove()
+
+ for (const link of links) {
+ const htmlLink = document.createElement('link')
+ htmlLink.rel = link.rel
+ htmlLink.href = link.href
+
+ if (link.type !== undefined) {
+ htmlLink.type = link.type
+ }
+
+ if (link.sizes !== undefined) {
+ htmlLink.setAttribute('sizes', link.sizes)
+ }
+
+ document.head.appendChild(htmlLink)
+ }
+ }
+
setMetadata(login.metadata.AccountsUrl, config.ACCOUNTS_URL)
setMetadata(presentation.metadata.UploadURL, config.UPLOAD_URL)
setMetadata(presentation.metadata.CollaboratorUrl, config.COLLABORATOR_URL)
@@ -187,8 +236,8 @@ export async function configurePlatform() {
setMetadata(uiPlugin.metadata.DefaultApplication, login.component.LoginApp)
- setMetadata(contactPlugin.metadata.LastNameFirst, config.LAST_NAME_FIRST === 'true' ?? false)
- const languages = config.LANGUAGES ? (config.LANGUAGES as string).split(',').map((l) => l.trim()) : ['en', 'ru', 'es', 'pt']
+ setMetadata(contactPlugin.metadata.LastNameFirst, myBranding.lastNameFirst === 'true' ?? false)
+ const languages = myBranding.languages ? (myBranding.languages as string).split(',').map((l) => l.trim()) : ['en', 'ru', 'es', 'pt']
setMetadata(uiPlugin.metadata.Languages, languages)
setMetadata(
@@ -245,10 +294,10 @@ export async function configurePlatform() {
// Disable for now, since it causes performance issues on linux/docker/kubernetes boxes for now.
setMetadata(client.metadata.UseProtocolCompression, true)
- setMetadata(uiPlugin.metadata.PlatformTitle, config.TITLE ?? 'Platform')
- setMetadata(workbench.metadata.PlatformTitle, config.TITLE ?? 'Platform')
- setDefaultLanguage(config.DEFAULT_LANGUAGE ?? 'en')
- setMetadata(workbench.metadata.DefaultApplication, 'tracker')
- setMetadata(workbench.metadata.DefaultSpace, tracker.project.DefaultProject)
- setMetadata(workbench.metadata.DefaultSpecial, 'issues')
+ setMetadata(uiPlugin.metadata.PlatformTitle, title)
+ setMetadata(workbench.metadata.PlatformTitle, title)
+ setDefaultLanguage(myBranding.defaultLanguage ?? 'en')
+ setMetadata(workbench.metadata.DefaultApplication, myBranding.defaultApplication ?? 'tracker')
+ setMetadata(workbench.metadata.DefaultSpace, myBranding.defaultSpace ?? tracker.project.DefaultProject)
+ setMetadata(workbench.metadata.DefaultSpecial, myBranding.defaultSpecial ?? 'issues')
}
diff --git a/dev/tool/src/cleanOrphan.ts b/dev/tool/src/cleanOrphan.ts
index 44b1146329..2cf24e2227 100644
--- a/dev/tool/src/cleanOrphan.ts
+++ b/dev/tool/src/cleanOrphan.ts
@@ -70,6 +70,7 @@ export async function checkOrphanWorkspaces (
db,
client,
productId,
+ null,
ws.workspace,
storageAdapter
)
diff --git a/dev/tool/src/index.ts b/dev/tool/src/index.ts
index 5fd0e7069a..edaad27d29 100644
--- a/dev/tool/src/index.ts
+++ b/dev/tool/src/index.ts
@@ -183,7 +183,7 @@ export function devTool (
const { mongodbUri } = prepareTools()
await withDatabase(mongodbUri, async (db) => {
console.log(`creating account ${cmd.first as string} ${cmd.last as string} (${email})...`)
- await createAcc(toolCtx, db, productId, email, cmd.password, cmd.first, cmd.last, true)
+ await createAcc(toolCtx, db, productId, null, email, cmd.password, cmd.first, cmd.last, true)
})
})
@@ -234,7 +234,7 @@ export function devTool (
}
console.log('assigning to workspace', workspaceInfo)
try {
- await assignWorkspace(toolCtx, db, productId, email, workspaceInfo.workspace, AccountRole.User)
+ await assignWorkspace(toolCtx, db, productId, null, email, workspaceInfo.workspace, AccountRole.User)
} catch (err: any) {
console.error(err)
}
@@ -281,7 +281,8 @@ export function devTool (
.description('create workspace')
.requiredOption('-w, --workspaceName ', 'Workspace name')
.option('-e, --email ', 'Author email', 'platform@email.com')
- .action(async (workspace, cmd) => {
+ .option('-i, --init ', 'Init from workspace')
+ .action(async (workspace, cmd: { email: string, workspaceName: string, init?: string }) => {
const { mongodbUri, txes, version, migrateOperations } = prepareTools()
await withDatabase(mongodbUri, async (db) => {
await createWorkspace(
@@ -291,6 +292,7 @@ export function devTool (
migrateOperations,
db,
productId,
+ cmd.init !== undefined ? { initWorkspace: cmd.init } : null,
cmd.email,
cmd.workspaceName,
workspace
@@ -429,9 +431,9 @@ export function devTool (
return
}
if (cmd.full) {
- await dropWorkspaceFull(toolCtx, db, client, productId, workspace, storageAdapter)
+ await dropWorkspaceFull(toolCtx, db, client, productId, null, workspace, storageAdapter)
} else {
- await dropWorkspace(toolCtx, db, productId, workspace)
+ await dropWorkspace(toolCtx, db, productId, null, workspace)
}
})
})
@@ -447,9 +449,9 @@ export function devTool (
await withDatabase(mongodbUri, async (db, client) => {
for (const workspace of await listWorkspacesByAccount(db, productId, email)) {
if (cmd.full) {
- await dropWorkspaceFull(toolCtx, db, client, productId, workspace.workspace, storageAdapter)
+ await dropWorkspaceFull(toolCtx, db, client, productId, null, workspace.workspace, storageAdapter)
} else {
- await dropWorkspace(toolCtx, db, productId, workspace.workspace)
+ await dropWorkspace(toolCtx, db, productId, null, workspace.workspace)
}
}
})
@@ -480,7 +482,7 @@ export function devTool (
for (const ws of workspacesJSON) {
const lastVisit = Math.floor((Date.now() - ws.lastVisit) / 1000 / 3600 / 24)
if (lastVisit > 30) {
- await dropWorkspaceFull(toolCtx, db, client, productId, ws.workspace, storageAdapter)
+ await dropWorkspaceFull(toolCtx, db, client, productId, null, ws.workspace, storageAdapter)
}
}
})
@@ -575,7 +577,7 @@ export function devTool (
.action(async (email: string, cmd) => {
const { mongodbUri } = prepareTools()
await withDatabase(mongodbUri, async (db) => {
- await dropAccount(toolCtx, db, productId, email)
+ await dropAccount(toolCtx, db, productId, null, email)
})
})
diff --git a/pods/account/src/__start.ts b/pods/account/src/__start.ts
index 55fe29e9e9..3cbf6c20ee 100644
--- a/pods/account/src/__start.ts
+++ b/pods/account/src/__start.ts
@@ -13,6 +13,8 @@
// limitations under the License.
//
+import fs from 'fs'
+import { type BrandingMap } from '@hcengineering/account'
import { serveAccount } from '@hcengineering/account-service'
import { MeasureMetricsContext, newMetrics, type Tx } from '@hcengineering/core'
import builder, { getModelVersion, migrateOperations } from '@hcengineering/model-all'
@@ -24,4 +26,11 @@ const txes = JSON.parse(JSON.stringify(builder(enabled, disabled).getTxes())) as
const metricsContext = new MeasureMetricsContext('account', {}, {}, newMetrics())
-serveAccount(metricsContext, getModelVersion(), txes, migrateOperations, '')
+const brandingPath = process.env.BRANDING_PATH
+
+let brandings: BrandingMap = {}
+if (brandingPath !== undefined && brandingPath !== '') {
+ brandings = JSON.parse(fs.readFileSync(brandingPath, 'utf8'))
+}
+
+serveAccount(metricsContext, getModelVersion(), txes, migrateOperations, '', brandings)
diff --git a/pods/authProviders/src/github.ts b/pods/authProviders/src/github.ts
index a456fb44e4..4ae35ee341 100644
--- a/pods/authProviders/src/github.ts
+++ b/pods/authProviders/src/github.ts
@@ -47,14 +47,24 @@ export function registerGithub (
if (email !== undefined) {
try {
if (ctx.query?.state != null) {
- const loginInfo = await joinWithProvider(measureCtx, db, productId, email, first, last, ctx.query.state, {
- githubId: ctx.state.user.id
- })
+ const loginInfo = await joinWithProvider(
+ measureCtx,
+ db,
+ productId,
+ null,
+ email,
+ first,
+ last,
+ ctx.query.state,
+ {
+ githubId: ctx.state.user.id
+ }
+ )
if (ctx.session != null) {
ctx.session.loginInfo = loginInfo
}
} else {
- const loginInfo = await loginWithProvider(measureCtx, db, productId, email, first, last, {
+ const loginInfo = await loginWithProvider(measureCtx, db, productId, null, email, first, last, {
githubId: ctx.state.user.id
})
if (ctx.session != null) {
diff --git a/pods/authProviders/src/google.ts b/pods/authProviders/src/google.ts
index 78d42276e9..4734aff1bf 100644
--- a/pods/authProviders/src/google.ts
+++ b/pods/authProviders/src/google.ts
@@ -48,12 +48,21 @@ export function registerGoogle (
if (email !== undefined) {
try {
if (ctx.query?.state != null) {
- const loginInfo = await joinWithProvider(measureCtx, db, productId, email, first, last, ctx.query.state)
+ const loginInfo = await joinWithProvider(
+ measureCtx,
+ db,
+ productId,
+ null,
+ email,
+ first,
+ last,
+ ctx.query.state
+ )
if (ctx.session != null) {
ctx.session.loginInfo = loginInfo
}
} else {
- const loginInfo = await loginWithProvider(measureCtx, db, productId, email, first, last)
+ const loginInfo = await loginWithProvider(measureCtx, db, productId, null, email, first, last)
if (ctx.session != null) {
ctx.session.loginInfo = loginInfo
}
diff --git a/pods/front/Dockerfile b/pods/front/Dockerfile
index 9d8c4c8986..f40251f7ba 100644
--- a/pods/front/Dockerfile
+++ b/pods/front/Dockerfile
@@ -4,6 +4,7 @@ ENV NODE_ENV production
WORKDIR /app
RUN npm install --ignore-scripts=false --verbose sharp@v0.32.6 bufferutil utf-8-validate @mongodb-js/zstd --unsafe-perm
+
COPY bundle/bundle.js ./
COPY dist/ ./dist/
diff --git a/server/account-service/src/index.ts b/server/account-service/src/index.ts
index 0607c32be9..200176c80c 100644
--- a/server/account-service/src/index.ts
+++ b/server/account-service/src/index.ts
@@ -7,7 +7,8 @@ import account, {
UpgradeWorker,
accountId,
cleanInProgressWorkspaces,
- getMethods
+ getMethods,
+ type BrandingMap
} from '@hcengineering/account'
import accountEn from '@hcengineering/account/lang/en.json'
import accountRu from '@hcengineering/account/lang/ru.json'
@@ -34,8 +35,10 @@ export function serveAccount (
txes: Tx[],
migrateOperations: [string, MigrateOperation][],
productId: string,
+ brandings: BrandingMap,
onClose?: () => void
): void {
+ console.log('Starting account service with brandings: ', brandings)
const methods = getMethods(version, txes, migrateOperations)
const ACCOUNT_PORT = parseInt(process.env.ACCOUNT_PORT ?? '3000')
const dbUri = process.env.MONGO_URL
@@ -141,7 +144,14 @@ export function serveAccount (
client = await client
}
const db = client.db(ACCOUNT_DB)
- const result = await method(measureCtx, db, productId, request, token)
+
+ let host: string | undefined
+ const origin = ctx.request.headers.origin ?? ctx.request.headers.referer
+ if (origin !== undefined) {
+ host = new URL(origin).host
+ }
+ const branding = host !== undefined ? brandings[host] : null
+ const result = await method(measureCtx, db, productId, branding, request, token)
worker?.updateResponseStatistics(result)
ctx.body = result
diff --git a/server/account/src/__tests__/account.test_skip.ts b/server/account/src/__tests__/account.test_skip.ts
index 39df87a03a..da40c99894 100644
--- a/server/account/src/__tests__/account.test_skip.ts
+++ b/server/account/src/__tests__/account.test_skip.ts
@@ -51,7 +51,7 @@ describe('server', () => {
params: [workspace, 'ООО Рога и Копыта']
}
- const result = await methods.createWorkspace(metricsContext, db, '', request)
+ const result = await methods.createWorkspace(metricsContext, db, '', null, request)
expect(result.result).toBeDefined()
workspace = result.result as string
})
@@ -62,12 +62,12 @@ describe('server', () => {
params: ['andrey2', '123']
}
- const result = await methods.createAccount(metricsContext, db, '', request)
+ const result = await methods.createAccount(metricsContext, db, '', null, request)
expect(result.result).toBeDefined()
})
it('should not create, duplicate account', async () => {
- await methods.createAccount(metricsContext, db, '', {
+ await methods.createAccount(metricsContext, db, '', null, {
method: 'createAccount',
params: ['andrey', '123']
})
@@ -77,20 +77,20 @@ describe('server', () => {
params: ['andrey', '123']
}
- const result = await methods.createAccount(metricsContext, db, '', request)
+ const result = await methods.createAccount(metricsContext, db, '', null, request)
expect(result.error).toBeDefined()
})
it('should login', async () => {
- await methods.createAccount(metricsContext, db, '', {
+ await methods.createAccount(metricsContext, db, '', null, {
method: 'createAccount',
params: ['andrey', '123']
})
- await methods.createWorkspace(metricsContext, db, '', {
+ await methods.createWorkspace(metricsContext, db, '', null, {
method: 'createWorkspace',
params: [workspace, 'ООО Рога и Копыта']
})
- await methods.assignWorkspace(metricsContext, db, '', {
+ await methods.assignWorkspace(metricsContext, db, '', null, {
method: 'assignWorkspace',
params: ['andrey', workspace]
})
@@ -100,7 +100,7 @@ describe('server', () => {
params: ['andrey', '123', workspace]
}
- const result = await methods.login(metricsContext, db, '', request)
+ const result = await methods.login(metricsContext, db, '', null, request)
expect(result.result).toBeDefined()
})
@@ -110,7 +110,7 @@ describe('server', () => {
params: ['andrey', '123555', workspace]
}
- const result = await methods.login(metricsContext, db, '', request)
+ const result = await methods.login(metricsContext, db, '', null, request)
expect(result.error).toBeDefined()
})
@@ -120,7 +120,7 @@ describe('server', () => {
params: ['andrey1', '123555', workspace]
}
- const result = await methods.login(metricsContext, db, '', request)
+ const result = await methods.login(metricsContext, db, '', null, request)
expect(result.error).toBeDefined()
})
@@ -130,20 +130,20 @@ describe('server', () => {
params: ['andrey', '123', 'non-existent-workspace']
}
- const result = await methods.login(metricsContext, db, '', request)
+ const result = await methods.login(metricsContext, db, '', null, request)
expect(result.error).toBeDefined()
})
it('do remove workspace', async () => {
- await methods.createAccount(metricsContext, db, '', {
+ await methods.createAccount(metricsContext, db, '', null, {
method: 'createAccount',
params: ['andrey', '123']
})
- await methods.createWorkspace(metricsContext, db, '', {
+ await methods.createWorkspace(metricsContext, db, '', null, {
method: 'createWorkspace',
params: [workspace, 'ООО Рога и Копыта']
})
- await methods.assignWorkspace(metricsContext, db, '', {
+ await methods.assignWorkspace(metricsContext, db, '', null, {
method: 'assignWorkspace',
params: ['andrey', workspace]
})
@@ -152,7 +152,7 @@ describe('server', () => {
expect((await getAccount(db, 'andrey'))?.workspaces.length).toEqual(1)
expect((await getWorkspaceByUrl(db, '', workspace))?.accounts.length).toEqual(1)
- await methods.removeWorkspace(metricsContext, db, '', {
+ await methods.removeWorkspace(metricsContext, db, '', null, {
method: 'removeWorkspace',
params: ['andrey', workspace]
})
diff --git a/server/account/src/operations.ts b/server/account/src/operations.ts
index 00fb7e0880..3e12e66d5b 100644
--- a/server/account/src/operations.ts
+++ b/server/account/src/operations.ts
@@ -234,6 +234,7 @@ async function getAccountInfo (
ctx: MeasureContext,
db: Db,
productId: string,
+ branding: Branding | null,
email: string,
password: string
): Promise {
@@ -254,6 +255,7 @@ async function getAccountInfoByToken (
ctx: MeasureContext,
db: Db,
productId: string,
+ branding: Branding | null,
token: string
): Promise {
let email: string = ''
@@ -290,12 +292,13 @@ export async function login (
ctx: MeasureContext,
db: Db,
productId: string,
+ branding: Branding | null,
_email: string,
password: string
): Promise {
const email = cleanEmail(_email)
try {
- const info = await getAccountInfo(ctx, db, productId, email, password)
+ const info = await getAccountInfo(ctx, db, productId, branding, email, password)
const result = {
endpoint: getEndpoint(),
email,
@@ -330,6 +333,7 @@ export async function selectWorkspace (
ctx: MeasureContext,
db: Db,
productId: string,
+ branding: Branding | null,
token: string,
workspaceUrl: string,
allowAdmin: boolean = true
@@ -429,6 +433,7 @@ export async function join (
ctx: MeasureContext,
db: Db,
productId: string,
+ branding: Branding | null,
_email: string,
password: string,
inviteId: ObjectId
@@ -441,14 +446,15 @@ export async function join (
ctx,
db,
productId,
+ branding,
email,
workspace.name,
invite?.role ?? AccountRole.User,
invite?.personId
)
- const token = (await login(ctx, db, productId, email, password)).token
- const result = await selectWorkspace(ctx, db, productId, token, ws.workspaceUrl ?? ws.workspace)
+ const token = (await login(ctx, db, productId, branding, email, password)).token
+ const result = await selectWorkspace(ctx, db, productId, branding, token, ws.workspaceUrl ?? ws.workspace)
await useInvite(db, inviteId)
return result
}
@@ -476,7 +482,13 @@ export async function confirmEmail (db: Db, _email: string): Promise {
/**
* @public
*/
-export async function confirm (ctx: MeasureContext, db: Db, productId: string, token: string): Promise {
+export async function confirm (
+ ctx: MeasureContext,
+ db: Db,
+ productId: string,
+ branding: Branding | null,
+ token: string
+): Promise {
const decode = decodeToken(token)
const _email = decode.extra?.confirm
if (_email === undefined) {
@@ -495,7 +507,7 @@ export async function confirm (ctx: MeasureContext, db: Db, productId: string, t
return result
}
-async function sendConfirmation (productId: string, account: Account): Promise {
+async function sendConfirmation (productId: string, branding: Branding | null, account: Account): Promise {
const sesURL = getMetadata(accountPlugin.metadata.SES_URL)
if (sesURL === undefined || sesURL === '') {
console.info('Please provide email service url to enable email confirmations.')
@@ -516,10 +528,11 @@ async function sendConfirmation (productId: string, account: Account): Promise {
decodeToken(token) // Just verify token is valid
@@ -739,7 +759,7 @@ export async function cleanInProgressWorkspaces (db: Db, productId: string): Pro
).map((it) => ({ ...it, productId }))
const ctx = new MeasureMetricsContext('clean', {})
for (const d of toDelete) {
- await dropWorkspace(ctx, db, productId, d.workspace)
+ await dropWorkspace(ctx, db, productId, null, d.workspace)
}
}
@@ -878,6 +898,7 @@ export async function createWorkspace (
migrationOperation: [string, MigrateOperation][],
db: Db,
productId: string,
+ branding: Branding | null,
email: string,
workspaceName: string,
workspace?: string,
@@ -915,7 +936,7 @@ export async function createWorkspace (
}
let model: Tx[] = []
try {
- const initWS = getMetadata(toolPlugin.metadata.InitWorkspace)
+ const initWS = branding?.initWorkspace ?? getMetadata(toolPlugin.metadata.InitWorkspace)
const wsId = getWorkspaceId(workspaceInfo.workspace, productId)
// We should not try to clone INIT_WS into INIT_WS during it's creation.
@@ -1034,7 +1055,14 @@ export async function upgradeWorkspace (
*/
export const createUserWorkspace =
(version: Data, txes: Tx[], migrationOperation: [string, MigrateOperation][]) =>
- async (ctx: MeasureContext, db: Db, productId: string, token: string, workspaceName: string): Promise => {
+ async (
+ ctx: MeasureContext,
+ db: Db,
+ productId: string,
+ branding: Branding | null,
+ token: string,
+ workspaceName: string
+ ): Promise => {
const { email } = decodeToken(token)
ctx.info('Creating workspace', { workspaceName, email })
@@ -1064,12 +1092,13 @@ export const createUserWorkspace =
migrationOperation,
db,
productId,
+ branding,
email,
workspaceName,
undefined,
notifyHandler,
async (workspace, model) => {
- const initWS = getMetadata(toolPlugin.metadata.InitWorkspace)
+ const initWS = branding?.initWorkspace ?? getMetadata(toolPlugin.metadata.InitWorkspace)
const shouldUpdateAccount = initWS !== undefined && (await getWorkspaceById(db, productId, initWS)) !== null
const client = await connect(
getTransactor(),
@@ -1085,6 +1114,7 @@ export const createUserWorkspace =
ctx,
db,
productId,
+ branding,
email,
workspace.workspace,
AccountRole.Owner,
@@ -1145,6 +1175,7 @@ export async function getInviteLink (
ctx: MeasureContext,
db: Db,
productId: string,
+ branding: Branding | null,
token: string,
exp: number,
emailMask: string,
@@ -1202,6 +1233,7 @@ export async function getUserWorkspaces (
ctx: MeasureContext,
db: Db,
productId: string,
+ branding: Branding | null,
token: string
): Promise {
const { email } = decodeToken(token)
@@ -1227,6 +1259,7 @@ export async function getWorkspaceInfo (
ctx: MeasureContext,
db: Db,
productId: string,
+ branding: Branding | null,
token: string,
_updateLastVisit: boolean = false
): Promise {
@@ -1337,10 +1370,11 @@ export async function createMissingEmployee (
ctx: MeasureContext,
db: Db,
productId: string,
+ branding: Branding | null,
token: string
): Promise {
const { email } = decodeToken(token)
- const wsInfo = await getWorkspaceInfo(ctx, db, productId, token)
+ const wsInfo = await getWorkspaceInfo(ctx, db, productId, branding, token)
const account = await getAccount(db, email)
if (account === null) {
@@ -1357,6 +1391,7 @@ export async function assignWorkspace (
ctx: MeasureContext,
db: Db,
productId: string,
+ branding: Branding | null,
_email: string,
workspaceId: string,
role: AccountRole,
@@ -1366,7 +1401,7 @@ export async function assignWorkspace (
personAccountId?: Ref
): Promise {
const email = cleanEmail(_email)
- const initWS = getMetadata(toolPlugin.metadata.InitWorkspace)
+ const initWS = branding?.initWorkspace ?? getMetadata(toolPlugin.metadata.InitWorkspace)
if (initWS !== undefined && initWS === workspaceId) {
Analytics.handleError(new Error(`assign-workspace failed ${email} ${workspaceId}`))
ctx.error('assign-workspace failed', { email, workspaceId, reason: 'initWs === workspaceId' })
@@ -1571,12 +1606,13 @@ export async function changePassword (
ctx: MeasureContext,
db: Db,
productId: string,
+ branding: Branding | null,
token: string,
oldPassword: string,
password: string
): Promise {
const { email } = decodeToken(token)
- const account = await getAccountInfo(ctx, db, productId, email, oldPassword)
+ const account = await getAccountInfo(ctx, db, productId, branding, email, oldPassword)
const salt = randomBytes(32)
const hash = hashWithSalt(password, salt)
@@ -1611,7 +1647,13 @@ export async function replacePassword (db: Db, productId: string, email: string,
/**
* @public
*/
-export async function requestPassword (ctx: MeasureContext, db: Db, productId: string, _email: string): Promise {
+export async function requestPassword (
+ ctx: MeasureContext,
+ db: Db,
+ productId: string,
+ branding: Branding | null,
+ _email: string
+): Promise {
const email = cleanEmail(_email)
const account = await getAccount(db, email)
@@ -1638,10 +1680,10 @@ export async function requestPassword (ctx: MeasureContext, db: Db, productId: s
)
const link = concatLink(front, `/login/recovery?id=${token}`)
-
- const text = await translate(accountPlugin.string.RecoveryText, { link })
- const html = await translate(accountPlugin.string.RecoveryHTML, { link })
- const subject = await translate(accountPlugin.string.RecoverySubject, {})
+ const lang = branding?.language
+ const text = await translate(accountPlugin.string.RecoveryText, { link }, lang)
+ const html = await translate(accountPlugin.string.RecoveryHTML, { link }, lang)
+ const subject = await translate(accountPlugin.string.RecoverySubject, {}, lang)
const to = account.email
await fetch(concatLink(sesURL, '/send'), {
@@ -1666,6 +1708,7 @@ export async function restorePassword (
ctx: MeasureContext,
db: Db,
productId: string,
+ branding: Branding | null,
token: string,
password: string
): Promise {
@@ -1682,7 +1725,7 @@ export async function restorePassword (
await updatePassword(db, account, password)
- return await login(ctx, db, productId, email, password)
+ return await login(ctx, db, productId, branding, email, password)
}
async function updatePassword (db: Db, account: Account, password: string | null): Promise {
@@ -1699,6 +1742,7 @@ export async function removeWorkspace (
ctx: MeasureContext,
db: Db,
productId: string,
+ branding: Branding | null,
email: string,
workspaceId: string
): Promise {
@@ -1719,6 +1763,7 @@ export async function checkJoin (
ctx: MeasureContext,
db: Db,
productId: string,
+ branding: Branding | null,
token: string,
inviteId: ObjectId
): Promise {
@@ -1732,7 +1777,7 @@ export async function checkJoin (
new Status(Severity.ERROR, platform.status.WorkspaceNotFound, { workspace: workspace.name })
)
}
- return await selectWorkspace(ctx, db, productId, token, ws?.workspaceUrl ?? ws.workspace, false)
+ return await selectWorkspace(ctx, db, productId, branding, token, ws?.workspaceUrl ?? ws.workspace, false)
}
/**
@@ -1742,6 +1787,7 @@ export async function dropWorkspace (
ctx: MeasureContext,
db: Db,
productId: string,
+ branding: Branding | null,
workspaceId: string
): Promise {
const ws = await getWorkspaceById(db, productId, workspaceId)
@@ -1765,10 +1811,11 @@ export async function dropWorkspaceFull (
db: Db,
client: MongoClient,
productId: string,
+ branding: Branding | null,
workspaceId: string,
storageAdapter?: StorageAdapter
): Promise {
- const ws = await dropWorkspace(ctx, db, productId, workspaceId)
+ const ws = await dropWorkspace(ctx, db, productId, branding, workspaceId)
const workspaceDb = client.db(ws.workspace)
await workspaceDb.dropDatabase()
const wspace = getWorkspaceId(workspaceId, productId)
@@ -1782,7 +1829,13 @@ export async function dropWorkspaceFull (
/**
* @public
*/
-export async function dropAccount (ctx: MeasureContext, db: Db, productId: string, email: string): Promise {
+export async function dropAccount (
+ ctx: MeasureContext,
+ db: Db,
+ productId: string,
+ branding: Branding | null,
+ email: string
+): Promise {
const account = await getAccount(db, email)
if (account === null) {
throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotFound, { account: email }))
@@ -1813,6 +1866,7 @@ export async function leaveWorkspace (
ctx: MeasureContext,
db: Db,
productId: string,
+ branding: Branding | null,
token: string,
email: string
): Promise {
@@ -1851,6 +1905,7 @@ export async function sendInvite (
ctx: MeasureContext,
db: Db,
productId: string,
+ branding: Branding | null,
token: string,
email: string,
personId?: Ref,
@@ -1885,13 +1940,14 @@ export async function sendInvite (
const expHours = 48
const exp = expHours * 60 * 60 * 1000
- const inviteId = await getInviteLink(ctx, db, productId, token, exp, email, 1)
+ const inviteId = await getInviteLink(ctx, db, productId, branding, token, exp, email, 1)
const link = concatLink(front, `/login/join?inviteId=${inviteId.toString()}`)
const ws = workspace.workspaceName ?? workspace.workspace
- const text = await translate(accountPlugin.string.InviteText, { link, ws, expHours })
- const html = await translate(accountPlugin.string.InviteHTML, { link, ws, expHours })
- const subject = await translate(accountPlugin.string.InviteSubject, { ws })
+ const lang = branding?.language
+ const text = await translate(accountPlugin.string.InviteText, { link, ws, expHours }, lang)
+ const html = await translate(accountPlugin.string.InviteHTML, { link, ws, expHours }, lang)
+ const subject = await translate(accountPlugin.string.InviteSubject, { ws }, lang)
const to = email
await fetch(concatLink(sesURL, '/send'), {
@@ -1935,6 +1991,14 @@ async function deactivatePersonAccount (
}
}
+export interface Branding {
+ title?: string
+ language?: string
+ initWorkspace?: string
+}
+
+export type BrandingMap = Record
+
/**
* @public
*/
@@ -1942,16 +2006,30 @@ export type AccountMethod = (
ctx: MeasureContext,
db: Db,
productId: string,
+ branding: Branding | null,
request: any,
token?: string
) => Promise
function wrap (
- accountMethod: (ctx: MeasureContext, db: Db, productId: string, ...args: any[]) => Promise
+ accountMethod: (
+ ctx: MeasureContext,
+ db: Db,
+ productId: string,
+ branding: Branding | null,
+ ...args: any[]
+ ) => Promise
): AccountMethod {
- return async function (ctx: MeasureContext, db: Db, productId: string, request: any, token?: string): Promise {
+ return async function (
+ ctx: MeasureContext,
+ db: Db,
+ productId: string,
+ branding: Branding | null,
+ request: any,
+ token?: string
+ ): Promise {
if (token !== undefined) request.params.unshift(token)
- return await accountMethod(ctx, db, productId, ...request.params)
+ return await accountMethod(ctx, db, productId, branding, ...request.params)
.then((result) => ({ id: request.id, result }))
.catch((err) => {
const status =
@@ -1975,6 +2053,7 @@ export async function joinWithProvider (
ctx: MeasureContext,
db: Db,
productId: string,
+ branding: Branding | null,
_email: string,
first: string,
last: string,
@@ -2013,29 +2092,39 @@ export async function joinWithProvider (
ctx,
db,
productId,
+ branding,
email,
workspace.name,
invite?.role ?? AccountRole.User,
invite?.personId
)
- const result = await selectWorkspace(ctx, db, productId, token, wsRes.workspaceUrl ?? wsRes.workspace, false)
+ const result = await selectWorkspace(
+ ctx,
+ db,
+ productId,
+ branding,
+ token,
+ wsRes.workspaceUrl ?? wsRes.workspace,
+ false
+ )
await useInvite(db, inviteId)
return result
}
- const newAccount = await createAcc(ctx, db, productId, email, null, first, last, true, extra)
+ const newAccount = await createAcc(ctx, db, productId, branding, email, null, first, last, true, extra)
const token = generateToken(email, getWorkspaceId('', productId), getExtra(newAccount))
const ws = await assignWorkspace(
ctx,
db,
productId,
+ branding,
email,
workspace.name,
invite?.role ?? AccountRole.User,
invite?.personId
)
- const result = await selectWorkspace(ctx, db, productId, token, ws.workspaceUrl ?? ws.workspace, false)
+ const result = await selectWorkspace(ctx, db, productId, branding, token, ws.workspaceUrl ?? ws.workspace, false)
await useInvite(db, inviteId)
@@ -2046,6 +2135,7 @@ export async function loginWithProvider (
ctx: MeasureContext,
db: Db,
productId: string,
+ branding: Branding | null,
_email: string,
first: string,
last: string,
@@ -2071,7 +2161,7 @@ export async function loginWithProvider (
}
return result
}
- const newAccount = await createAcc(ctx, db, productId, email, null, first, last, true, extra)
+ const newAccount = await createAcc(ctx, db, productId, branding, email, null, first, last, true, extra)
const result = {
endpoint: getEndpoint(),
diff --git a/server/front/src/index.ts b/server/front/src/index.ts
index a7dcb9d2db..b18e7fac57 100644
--- a/server/front/src/index.ts
+++ b/server/front/src/index.ts
@@ -244,10 +244,7 @@ export function start (
calendarUrl: string
collaboratorUrl: string
collaboratorApiUrl: string
- title?: string
- languages: string
- defaultLanguage: string
- lastNameFirst?: string
+ brandingUrl?: string
},
port: number,
extraConfig?: Record
@@ -281,10 +278,7 @@ export function start (
CALENDAR_URL: config.calendarUrl,
COLLABORATOR_URL: config.collaboratorUrl,
COLLABORATOR_API_URL: config.collaboratorApiUrl,
- TITLE: config.title,
- LANGUAGES: config.languages,
- DEFAULT_LANGUAGE: config.defaultLanguage,
- LAST_NAME_FIRST: config.lastNameFirst,
+ BRANDING_URL: config.brandingUrl,
...(extraConfig ?? {})
}
res.set('Cache-Control', cacheControlNoCache)
diff --git a/server/front/src/starter.ts b/server/front/src/starter.ts
index 6d659d46d8..d1c855a5ab 100644
--- a/server/front/src/starter.ts
+++ b/server/front/src/starter.ts
@@ -22,8 +22,6 @@ import serverToken from '@hcengineering/server-token'
import { start } from '.'
export function startFront (ctx: MeasureContext, extraConfig?: Record): void {
- const defaultLanguage = process.env.DEFAULT_LANGUAGE ?? 'en'
- const languages = process.env.LANGUAGES ?? 'en,ru'
const SERVER_PORT = parseInt(process.env.SERVER_PORT ?? '8080')
const transactorEndpoint = process.env.TRANSACTOR_URL
@@ -107,9 +105,7 @@ export function startFront (ctx: MeasureContext, extraConfig?: Record this.page.locator('button[id$="Contacts"]')
buttonTracker = (): Locator => this.page.locator('button[id$="TrackerApplication"]')
buttonNotification = (): Locator => this.page.locator('button[id$="Inbox"]')
- buttonDocuments = (): Locator => this.page.locator('button[id$="DocumentApplication"]')
+ buttonDocuments = (): Locator => this.page.locator('button[id$="document:string:DocumentApplication"]')
profileButton = (): Locator => this.page.locator('#profile-button')
inviteToWorkspaceButton = (): Locator => this.page.locator('button:has-text("Invite to workspace")')
getInviteLinkButton = (): Locator => this.page.locator('button:has-text("Get invite link")')