mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-25 09:13:07 +03:00
UBERF-5140: Any workspace names (#4489)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
parent
27035518f5
commit
9dcec23815
@ -83,7 +83,7 @@ Before you can begin, you need to create a workspace and an account and associat
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd ./tool # dev/tool in the repository root
|
cd ./tool # dev/tool in the repository root
|
||||||
rushx run-local create-workspace ws1 -o DevWorkspace # Create workspace
|
rushx run-local create-workspace ws1 -w DevWorkspace # Create workspace
|
||||||
rushx run-local create-account user1 -p 1234 -f John -l Appleseed # Create account
|
rushx run-local create-account user1 -p 1234 -f John -l Appleseed # Create account
|
||||||
rushx run-local configure ws1 --list --enable '*' # Enable all modules, even if they are not yet intended to be used by a wide audience.
|
rushx run-local configure ws1 --list --enable '*' # Enable all modules, even if they are not yet intended to be used by a wide audience.
|
||||||
rushx run-local assign-workspace user1 ws1 # Assign workspace to user.
|
rushx run-local assign-workspace user1 ws1 # Assign workspace to user.
|
||||||
|
@ -155,7 +155,7 @@ export async function connect (handler: (tx: Tx) => void): Promise<ClientConnect
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
defaultContentAdapter: 'default',
|
defaultContentAdapter: 'default',
|
||||||
workspace: getWorkspaceId('')
|
workspace: { ...getWorkspaceId(''), workspaceUrl: '', workspaceName: '' }
|
||||||
}
|
}
|
||||||
const ctx = new MeasureMetricsContext('client', {})
|
const ctx = new MeasureMetricsContext('client', {})
|
||||||
const serverStorage = await createServerStorage(ctx, conf, {
|
const serverStorage = await createServerStorage(ctx, conf, {
|
||||||
|
@ -46,7 +46,6 @@ services:
|
|||||||
links:
|
links:
|
||||||
- mongodb
|
- mongodb
|
||||||
- minio
|
- minio
|
||||||
- transactor
|
|
||||||
ports:
|
ports:
|
||||||
- 3000:3000
|
- 3000:3000
|
||||||
environment:
|
environment:
|
||||||
@ -136,6 +135,7 @@ services:
|
|||||||
- elastic
|
- elastic
|
||||||
- minio
|
- minio
|
||||||
- rekoni
|
- rekoni
|
||||||
|
- account
|
||||||
# - apm-server
|
# - apm-server
|
||||||
ports:
|
ports:
|
||||||
- 3333:3333
|
- 3333:3333
|
||||||
@ -154,6 +154,7 @@ services:
|
|||||||
- FRONT_URL=http://localhost:8087
|
- FRONT_URL=http://localhost:8087
|
||||||
# - APM_SERVER_URL=http://apm-server:8200
|
# - APM_SERVER_URL=http://apm-server:8200
|
||||||
- SERVER_PROVIDER=ws
|
- SERVER_PROVIDER=ws
|
||||||
|
- ACCOUNTS_URL=http://account:3000
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
rekoni:
|
rekoni:
|
||||||
image: hardcoreeng/rekoni-service
|
image: hardcoreeng/rekoni-service
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
import { DOMAIN_TX, getWorkspaceId, MeasureMetricsContext } from '@hcengineering/core'
|
import { DOMAIN_TX, MeasureMetricsContext } from '@hcengineering/core'
|
||||||
import { createInMemoryTxAdapter } from '@hcengineering/dev-storage'
|
import { createInMemoryTxAdapter } from '@hcengineering/dev-storage'
|
||||||
import {
|
import {
|
||||||
ContentTextAdapter,
|
ContentTextAdapter,
|
||||||
@ -44,7 +44,7 @@ async function createNullContentTextAdapter (): Promise<ContentTextAdapter> {
|
|||||||
export async function start (port: number, host?: string): Promise<void> {
|
export async function start (port: number, host?: string): Promise<void> {
|
||||||
const ctx = new MeasureMetricsContext('server', {})
|
const ctx = new MeasureMetricsContext('server', {})
|
||||||
startJsonRpc(ctx, {
|
startJsonRpc(ctx, {
|
||||||
pipelineFactory: (ctx) => {
|
pipelineFactory: (ctx, workspaceId) => {
|
||||||
const conf: DbConfiguration = {
|
const conf: DbConfiguration = {
|
||||||
domains: {
|
domains: {
|
||||||
[DOMAIN_TX]: 'InMemoryTx'
|
[DOMAIN_TX]: 'InMemoryTx'
|
||||||
@ -74,13 +74,14 @@ export async function start (port: number, host?: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
defaultContentAdapter: 'default',
|
defaultContentAdapter: 'default',
|
||||||
workspace: getWorkspaceId('')
|
workspace: workspaceId
|
||||||
}
|
}
|
||||||
return createPipeline(ctx, conf, [], false, () => {})
|
return createPipeline(ctx, conf, [], false, () => {})
|
||||||
},
|
},
|
||||||
sessionFactory: (token, pipeline, broadcast) => new ClientSession(broadcast, token, pipeline),
|
sessionFactory: (token, pipeline, broadcast) => new ClientSession(broadcast, token, pipeline),
|
||||||
port,
|
port,
|
||||||
productId: '',
|
productId: '',
|
||||||
serverFactory: startHttpServer
|
serverFactory: startHttpServer,
|
||||||
|
accountsUrl: ''
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -207,14 +207,19 @@ export async function benchmark (
|
|||||||
operations = 0
|
operations = 0
|
||||||
requestTime = 0
|
requestTime = 0
|
||||||
transfer = 0
|
transfer = 0
|
||||||
for (const w of workspaceId) {
|
const r = extract(
|
||||||
const r = extract(json.metrics as Metrics, w.name, 'client', 'handleRequest', 'process', 'find-all')
|
json.metrics as Metrics,
|
||||||
|
'🧲 session',
|
||||||
|
'client',
|
||||||
|
'handleRequest',
|
||||||
|
'process',
|
||||||
|
'find-all'
|
||||||
|
)
|
||||||
operations += r?.operations ?? 0
|
operations += r?.operations ?? 0
|
||||||
requestTime += (r?.value ?? 0) / (((r?.operations as number) ?? 0) + 1)
|
requestTime += (r?.value ?? 0) / (((r?.operations as number) ?? 0) + 1)
|
||||||
|
|
||||||
const tr = extract(json.metrics as Metrics, w.name, 'client', 'handleRequest', '#send-data')
|
const tr = extract(json.metrics as Metrics, '🧲 session', 'client', 'handleRequest', '#send-data')
|
||||||
transfer += tr?.value ?? 0
|
transfer += tr?.value ?? 0
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.log(err)
|
console.log(err)
|
||||||
|
@ -17,20 +17,20 @@
|
|||||||
import {
|
import {
|
||||||
ACCOUNT_DB,
|
ACCOUNT_DB,
|
||||||
assignWorkspace,
|
assignWorkspace,
|
||||||
|
ClientWorkspaceInfo,
|
||||||
confirmEmail,
|
confirmEmail,
|
||||||
createAcc,
|
createAcc,
|
||||||
createWorkspace,
|
createWorkspace,
|
||||||
dropAccount,
|
dropAccount,
|
||||||
dropWorkspace,
|
dropWorkspace,
|
||||||
getAccount,
|
getAccount,
|
||||||
getWorkspace,
|
getWorkspaceById,
|
||||||
listAccounts,
|
listAccounts,
|
||||||
listWorkspaces,
|
listWorkspaces,
|
||||||
replacePassword,
|
replacePassword,
|
||||||
setAccountAdmin,
|
setAccountAdmin,
|
||||||
setRole,
|
setRole,
|
||||||
upgradeWorkspace,
|
upgradeWorkspace
|
||||||
WorkspaceInfoOnly
|
|
||||||
} from '@hcengineering/account'
|
} from '@hcengineering/account'
|
||||||
import { setMetadata } from '@hcengineering/platform'
|
import { setMetadata } from '@hcengineering/platform'
|
||||||
import {
|
import {
|
||||||
@ -43,7 +43,7 @@ import {
|
|||||||
import serverToken, { decodeToken, generateToken } from '@hcengineering/server-token'
|
import serverToken, { decodeToken, generateToken } from '@hcengineering/server-token'
|
||||||
import toolPlugin, { FileModelLogger } from '@hcengineering/server-tool'
|
import toolPlugin, { FileModelLogger } from '@hcengineering/server-tool'
|
||||||
|
|
||||||
import { program, Command } from 'commander'
|
import { Command, program } from 'commander'
|
||||||
import { Db, MongoClient } from 'mongodb'
|
import { Db, MongoClient } from 'mongodb'
|
||||||
import { clearTelegramHistory } from './telegram'
|
import { clearTelegramHistory } from './telegram'
|
||||||
import { diffWorkspace } from './workspace'
|
import { diffWorkspace } from './workspace'
|
||||||
@ -155,7 +155,16 @@ export function devTool (
|
|||||||
const { mongodbUri } = prepareTools()
|
const { mongodbUri } = prepareTools()
|
||||||
await withDatabase(mongodbUri, async (db, client) => {
|
await withDatabase(mongodbUri, async (db, client) => {
|
||||||
console.log(`assigning user ${email} to ${workspace}...`)
|
console.log(`assigning user ${email} to ${workspace}...`)
|
||||||
await assignWorkspace(db, productId, email, workspace)
|
const workspaceInfo = await getWorkspaceById(db, productId, workspace)
|
||||||
|
if (workspaceInfo === null) {
|
||||||
|
throw new Error(`workspace ${workspace} not found`)
|
||||||
|
}
|
||||||
|
console.log('assigning to workspace', workspaceInfo)
|
||||||
|
try {
|
||||||
|
await assignWorkspace(db, productId, email, workspaceInfo?.workspaceUrl ?? workspaceInfo.workspace)
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -197,11 +206,12 @@ export function devTool (
|
|||||||
program
|
program
|
||||||
.command('create-workspace <name>')
|
.command('create-workspace <name>')
|
||||||
.description('create workspace')
|
.description('create workspace')
|
||||||
.requiredOption('-o, --organization <organization>', 'organization name')
|
.requiredOption('-w, --workspaceName <workspaceName>', 'Workspace name')
|
||||||
|
.option('-e, --email <email>', 'Author email', 'platform@email.com')
|
||||||
.action(async (workspace, cmd) => {
|
.action(async (workspace, cmd) => {
|
||||||
const { mongodbUri, txes, version, migrateOperations } = prepareTools()
|
const { mongodbUri, txes, version, migrateOperations } = prepareTools()
|
||||||
await withDatabase(mongodbUri, async (db) => {
|
await withDatabase(mongodbUri, async (db) => {
|
||||||
await createWorkspace(version, txes, migrateOperations, db, productId, workspace, cmd.organization)
|
await createWorkspace(version, txes, migrateOperations, db, productId, cmd.email, cmd.workspaceName, workspace)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -230,7 +240,11 @@ export function devTool (
|
|||||||
.action(async (workspace, cmd) => {
|
.action(async (workspace, cmd) => {
|
||||||
const { mongodbUri, version, txes, migrateOperations } = prepareTools()
|
const { mongodbUri, version, txes, migrateOperations } = prepareTools()
|
||||||
await withDatabase(mongodbUri, async (db) => {
|
await withDatabase(mongodbUri, async (db) => {
|
||||||
await upgradeWorkspace(version, txes, migrateOperations, productId, db, workspace)
|
const info = await getWorkspaceById(db, productId, workspace)
|
||||||
|
if (info === null) {
|
||||||
|
throw new Error(`workspace ${workspace} not found`)
|
||||||
|
}
|
||||||
|
await upgradeWorkspace(version, txes, migrateOperations, productId, db, info.workspaceUrl ?? info.workspace)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -252,7 +266,7 @@ export function devTool (
|
|||||||
const workspaces = await listWorkspaces(db, productId)
|
const workspaces = await listWorkspaces(db, productId)
|
||||||
const withError: string[] = []
|
const withError: string[] = []
|
||||||
|
|
||||||
async function _upgradeWorkspace (ws: WorkspaceInfoOnly): Promise<void> {
|
async function _upgradeWorkspace (ws: ClientWorkspaceInfo): Promise<void> {
|
||||||
const t = Date.now()
|
const t = Date.now()
|
||||||
const logger = cmd.console
|
const logger = cmd.console
|
||||||
? consoleModelLogger
|
? consoleModelLogger
|
||||||
@ -298,7 +312,7 @@ export function devTool (
|
|||||||
.action(async (workspace, cmd) => {
|
.action(async (workspace, cmd) => {
|
||||||
const { mongodbUri } = prepareTools()
|
const { mongodbUri } = prepareTools()
|
||||||
await withDatabase(mongodbUri, async (db) => {
|
await withDatabase(mongodbUri, async (db) => {
|
||||||
const ws = await getWorkspace(db, productId, workspace)
|
const ws = await getWorkspaceById(db, productId, workspace)
|
||||||
if (ws === null) {
|
if (ws === null) {
|
||||||
console.log('no workspace exists')
|
console.log('no workspace exists')
|
||||||
return
|
return
|
||||||
|
@ -229,7 +229,7 @@ export async function createClient (
|
|||||||
let lastTx: number
|
let lastTx: number
|
||||||
|
|
||||||
function txHandler (tx: Tx): void {
|
function txHandler (tx: Tx): void {
|
||||||
if (tx === null) {
|
if (tx == null) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (client === null) {
|
if (client === null) {
|
||||||
|
@ -45,8 +45,8 @@ function count (): string {
|
|||||||
* @public
|
* @public
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function generateId<T extends Doc> (): Ref<T> {
|
export function generateId<T extends Doc> (join: string = ''): Ref<T> {
|
||||||
return (timestamp() + random + count()) as Ref<T>
|
return (timestamp() + join + random + join + count()) as Ref<T>
|
||||||
}
|
}
|
||||||
|
|
||||||
let currentAccount: Account
|
let currentAccount: Account
|
||||||
@ -89,6 +89,14 @@ export interface WorkspaceId {
|
|||||||
productId: string
|
productId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export interface WorkspaceIdWithUrl extends WorkspaceId {
|
||||||
|
workspaceUrl: string
|
||||||
|
workspaceName: string
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*
|
*
|
||||||
@ -507,7 +515,7 @@ export function cutObjectArray (obj: any): any {
|
|||||||
for (const key of Object.keys(obj)) {
|
for (const key of Object.keys(obj)) {
|
||||||
if (Array.isArray(obj[key])) {
|
if (Array.isArray(obj[key])) {
|
||||||
if (obj[key].length > 3) {
|
if (obj[key].length > 3) {
|
||||||
Object.assign(r, { [key]: `[${obj[key].slice(0, 3)}, ... and ${obj[key].length - 3} more]` })
|
Object.assign(r, { [key]: [...obj[key].slice(0, 3), `... and ${obj[key].length - 3} more`] })
|
||||||
} else Object.assign(r, { [key]: obj[key] })
|
} else Object.assign(r, { [key]: obj[key] })
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@ import core, {
|
|||||||
AccountClient,
|
AccountClient,
|
||||||
ClientConnectEvent,
|
ClientConnectEvent,
|
||||||
TxHandler,
|
TxHandler,
|
||||||
|
TxPersistenceStore,
|
||||||
TxWorkspaceEvent,
|
TxWorkspaceEvent,
|
||||||
WorkspaceEvent,
|
WorkspaceEvent,
|
||||||
createClient
|
createClient
|
||||||
@ -68,7 +69,17 @@ export default async () => {
|
|||||||
return connect(url.href, upgradeHandler, onUpgrade, onUnauthorized, onConnect)
|
return connect(url.href, upgradeHandler, onUpgrade, onUnauthorized, onConnect)
|
||||||
},
|
},
|
||||||
filterModel ? [...getPlugins(), ...(getMetadata(clientPlugin.metadata.ExtraPlugins) ?? [])] : undefined,
|
filterModel ? [...getPlugins(), ...(getMetadata(clientPlugin.metadata.ExtraPlugins) ?? [])] : undefined,
|
||||||
{
|
createModelPersistence(token)
|
||||||
|
)
|
||||||
|
// Check if we had dev hook for client.
|
||||||
|
client = hookClient(client)
|
||||||
|
return await client
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function createModelPersistence (token: string): TxPersistenceStore | undefined {
|
||||||
|
return {
|
||||||
load: async () => {
|
load: async () => {
|
||||||
if (typeof localStorage !== 'undefined') {
|
if (typeof localStorage !== 'undefined') {
|
||||||
const storedValue = localStorage.getItem('platform.model') ?? null
|
const storedValue = localStorage.getItem('platform.model') ?? null
|
||||||
@ -96,14 +107,8 @@ export default async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
// Check if we had dev hook for client.
|
|
||||||
client = hookClient(client)
|
|
||||||
return await client
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function hookClient (client: Promise<AccountClient>): Promise<AccountClient> {
|
async function hookClient (client: Promise<AccountClient>): Promise<AccountClient> {
|
||||||
const hook = getMetadata(clientPlugin.metadata.ClientHook)
|
const hook = getMetadata(clientPlugin.metadata.ClientHook)
|
||||||
if (hook !== undefined) {
|
if (hook !== undefined) {
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
"Join": "Join",
|
"Join": "Join",
|
||||||
"Email": "Email",
|
"Email": "Email",
|
||||||
"Password": "Password",
|
"Password": "Password",
|
||||||
"Workspace": "Workspace",
|
"Workspace": "Workspace name",
|
||||||
"DoNotHaveAnAccount": "Do not have an account?",
|
"DoNotHaveAnAccount": "Do not have an account?",
|
||||||
"PasswordRepeat": "Repeat password",
|
"PasswordRepeat": "Repeat password",
|
||||||
"HaveAccount": "Already have an account?",
|
"HaveAccount": "Already have an account?",
|
||||||
@ -27,12 +27,6 @@
|
|||||||
"WantAnotherWorkspace": "Want to create another workspace?",
|
"WantAnotherWorkspace": "Want to create another workspace?",
|
||||||
"ChangeAccount": "Change account",
|
"ChangeAccount": "Change account",
|
||||||
"NotSeeingWorkspace": "Not seeing your workspace?",
|
"NotSeeingWorkspace": "Not seeing your workspace?",
|
||||||
"WorkspaceNameRule": "Workspace name cannot contain special characters except -",
|
|
||||||
"WorkspaceNameRuleCapital": "The workspace name must contain only lowercase letters",
|
|
||||||
"WorkspaceNameRuleHyphen": "The workspace name cannot start with a dash (-)",
|
|
||||||
"WorkspaceNameRuleHyphenEnd": "The workspace name cannot end with a dash (-)",
|
|
||||||
"WorkspaceNameRuleLengthLow": "Workspace name must be at least 3 characters",
|
|
||||||
"WorkspaceNameRuleLengthHigh": "Workspace name must be no longer than 63 characters",
|
|
||||||
"ForgotPassword": "Forgot your password?",
|
"ForgotPassword": "Forgot your password?",
|
||||||
"KnowPassword": "Know your password?",
|
"KnowPassword": "Know your password?",
|
||||||
"Recover": "Recover",
|
"Recover": "Recover",
|
||||||
|
@ -27,12 +27,6 @@
|
|||||||
"WantAnotherWorkspace": "Хотите создать другое рабочее пространство?",
|
"WantAnotherWorkspace": "Хотите создать другое рабочее пространство?",
|
||||||
"ChangeAccount": "Сменить пользователя",
|
"ChangeAccount": "Сменить пользователя",
|
||||||
"NotSeeingWorkspace": "Не видите ваше рабочее пространство?",
|
"NotSeeingWorkspace": "Не видите ваше рабочее пространство?",
|
||||||
"WorkspaceNameRule": "Название рабочего пространства не может содержать специальные символы кроме -",
|
|
||||||
"WorkspaceNameRuleCapital": "Название рабочего пространства должно содержать только строчные буквы",
|
|
||||||
"WorkspaceNameRuleHyphen": "Название рабочего пространства не может начинаться с символа 'тире' (-)",
|
|
||||||
"WorkspaceNameRuleHyphenEnd": "Название рабочего пространства не может заканчиваться символом 'тире' (-)",
|
|
||||||
"WorkspaceNameRuleLengthLow": "Название рабочего пространства должно быть не короче 3 символов",
|
|
||||||
"WorkspaceNameRuleLengthHigh": "Название рабочего пространства должно быть не длиннее 63 символов",
|
|
||||||
"ForgotPassword": "Забыли пароль?",
|
"ForgotPassword": "Забыли пароль?",
|
||||||
"KnowPassword": "Знаете пароль?",
|
"KnowPassword": "Знаете пароль?",
|
||||||
"Recover": "Восстановить",
|
"Recover": "Восстановить",
|
||||||
|
@ -31,7 +31,7 @@
|
|||||||
navigate(loc)
|
navigate(loc)
|
||||||
}
|
}
|
||||||
|
|
||||||
function goToLogin () {
|
function goToLogin (): void {
|
||||||
const loc = getCurrentLocation()
|
const loc = getCurrentLocation()
|
||||||
loc.query = undefined
|
loc.query = undefined
|
||||||
loc.path[1] = 'login'
|
loc.path[1] = 'login'
|
||||||
@ -39,7 +39,7 @@
|
|||||||
navigate(loc)
|
navigate(loc)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function check () {
|
async function check (): Promise<void> {
|
||||||
const location = getCurrentLocation()
|
const location = getCurrentLocation()
|
||||||
if (location.query?.id === undefined || location.query?.id === null) return
|
if (location.query?.id === undefined || location.query?.id === null) return
|
||||||
status = new Status(Severity.INFO, login.status.ConnectingToServer, {})
|
status = new Status(Severity.INFO, login.status.ConnectingToServer, {})
|
||||||
@ -50,6 +50,7 @@
|
|||||||
|
|
||||||
if (result !== undefined) {
|
if (result !== undefined) {
|
||||||
setMetadata(presentation.metadata.Token, result.token)
|
setMetadata(presentation.metadata.Token, result.token)
|
||||||
|
setMetadataLocalStorage(login.metadata.LastToken, result.token)
|
||||||
setMetadataLocalStorage(login.metadata.LoginEndpoint, result.endpoint)
|
setMetadataLocalStorage(login.metadata.LoginEndpoint, result.endpoint)
|
||||||
setMetadataLocalStorage(login.metadata.LoginEmail, result.email)
|
setMetadataLocalStorage(login.metadata.LoginEmail, result.email)
|
||||||
goToWorkspaces()
|
goToWorkspaces()
|
||||||
|
@ -28,38 +28,7 @@
|
|||||||
{
|
{
|
||||||
name: 'workspace',
|
name: 'workspace',
|
||||||
i18n: login.string.Workspace,
|
i18n: login.string.Workspace,
|
||||||
rules: [
|
rules: []
|
||||||
{
|
|
||||||
rule: /^-/,
|
|
||||||
notMatch: true,
|
|
||||||
ruleDescr: login.string.WorkspaceNameRuleHyphen
|
|
||||||
},
|
|
||||||
{
|
|
||||||
rule: /-$/,
|
|
||||||
notMatch: true,
|
|
||||||
ruleDescr: login.string.WorkspaceNameRuleHyphenEnd
|
|
||||||
},
|
|
||||||
{
|
|
||||||
rule: /[A-Z]/,
|
|
||||||
notMatch: true,
|
|
||||||
ruleDescr: login.string.WorkspaceNameRuleCapital
|
|
||||||
},
|
|
||||||
{
|
|
||||||
rule: /^[0-9a-z-]+$/,
|
|
||||||
notMatch: false,
|
|
||||||
ruleDescr: login.string.WorkspaceNameRule
|
|
||||||
},
|
|
||||||
{
|
|
||||||
rule: /^[0-9a-z-]{3,}$/,
|
|
||||||
notMatch: false,
|
|
||||||
ruleDescr: login.string.WorkspaceNameRuleLengthLow
|
|
||||||
},
|
|
||||||
{
|
|
||||||
rule: /^[0-9a-z-]{3,63}$/,
|
|
||||||
notMatch: false,
|
|
||||||
ruleDescr: login.string.WorkspaceNameRuleLengthHigh
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -89,12 +58,13 @@
|
|||||||
|
|
||||||
if (result !== undefined) {
|
if (result !== undefined) {
|
||||||
setMetadata(presentation.metadata.Token, result.token)
|
setMetadata(presentation.metadata.Token, result.token)
|
||||||
|
setMetadataLocalStorage(login.metadata.LastToken, result.token)
|
||||||
const tokens: Record<string, string> = fetchMetadataLocalStorage(login.metadata.LoginTokens) ?? {}
|
const tokens: Record<string, string> = fetchMetadataLocalStorage(login.metadata.LoginTokens) ?? {}
|
||||||
tokens[object.workspace] = result.token
|
tokens[object.workspace] = result.token
|
||||||
setMetadataLocalStorage(login.metadata.LoginTokens, tokens)
|
setMetadataLocalStorage(login.metadata.LoginTokens, tokens)
|
||||||
setMetadataLocalStorage(login.metadata.LoginEndpoint, result.endpoint)
|
setMetadataLocalStorage(login.metadata.LoginEndpoint, result.endpoint)
|
||||||
setMetadataLocalStorage(login.metadata.LoginEmail, result.email)
|
setMetadataLocalStorage(login.metadata.LoginEmail, result.email)
|
||||||
navigate({ path: [workbenchId, object.workspace] })
|
navigate({ path: [workbenchId, result.workspace] })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -72,7 +72,7 @@
|
|||||||
for (const field of fields) {
|
for (const field of fields) {
|
||||||
const v = object[field.name]
|
const v = object[field.name]
|
||||||
const f = field
|
const f = field
|
||||||
if (!f.optional && (!v || v === '')) {
|
if (!f.optional && (!v || v.trim() === '')) {
|
||||||
status = new Status(Severity.INFO, login.status.RequiredField, {
|
status = new Status(Severity.INFO, login.status.RequiredField, {
|
||||||
field: await translate(field.i18n, {}, language)
|
field: await translate(field.i18n, {}, language)
|
||||||
})
|
})
|
||||||
|
@ -75,6 +75,7 @@
|
|||||||
|
|
||||||
if (result !== undefined) {
|
if (result !== undefined) {
|
||||||
setMetadata(presentation.metadata.Token, result.token)
|
setMetadata(presentation.metadata.Token, result.token)
|
||||||
|
setMetadataLocalStorage(login.metadata.LastToken, result.token)
|
||||||
const tokens: Record<string, string> = fetchMetadataLocalStorage(login.metadata.LoginTokens) ?? {}
|
const tokens: Record<string, string> = fetchMetadataLocalStorage(login.metadata.LoginTokens) ?? {}
|
||||||
tokens[result.workspace] = result.token
|
tokens[result.workspace] = result.token
|
||||||
setMetadataLocalStorage(login.metadata.LoginTokens, tokens)
|
setMetadataLocalStorage(login.metadata.LoginTokens, tokens)
|
||||||
@ -128,10 +129,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
check()
|
void check()
|
||||||
})
|
})
|
||||||
|
|
||||||
async function check () {
|
async function check (): Promise<void> {
|
||||||
if (location.query?.inviteId === undefined || location.query?.inviteId === null) return
|
if (location.query?.inviteId === undefined || location.query?.inviteId === null) return
|
||||||
status = new Status(Severity.INFO, login.status.ConnectingToServer, {})
|
status = new Status(Severity.INFO, login.status.ConnectingToServer, {})
|
||||||
const [, result] = await checkJoined(location.query.inviteId)
|
const [, result] = await checkJoined(location.query.inviteId)
|
||||||
@ -139,6 +140,7 @@
|
|||||||
if (result !== undefined) {
|
if (result !== undefined) {
|
||||||
const tokens: Record<string, string> = fetchMetadataLocalStorage(login.metadata.LoginTokens) ?? {}
|
const tokens: Record<string, string> = fetchMetadataLocalStorage(login.metadata.LoginTokens) ?? {}
|
||||||
setMetadata(presentation.metadata.Token, result.token)
|
setMetadata(presentation.metadata.Token, result.token)
|
||||||
|
setMetadataLocalStorage(login.metadata.LastToken, result.token)
|
||||||
tokens[result.workspace] = result.token
|
tokens[result.workspace] = result.token
|
||||||
setMetadataLocalStorage(login.metadata.LoginTokens, tokens)
|
setMetadataLocalStorage(login.metadata.LoginTokens, tokens)
|
||||||
setMetadataLocalStorage(login.metadata.LoginEndpoint, result.endpoint)
|
setMetadataLocalStorage(login.metadata.LoginEndpoint, result.endpoint)
|
||||||
|
@ -63,6 +63,7 @@
|
|||||||
onDestroy(
|
onDestroy(
|
||||||
location.subscribe((loc) => {
|
location.subscribe((loc) => {
|
||||||
void (async (loc) => {
|
void (async (loc) => {
|
||||||
|
token = getMetadata(presentation.metadata.Token)
|
||||||
page = loc.path[1] ?? (token ? 'selectWorkspace' : 'login')
|
page = loc.path[1] ?? (token ? 'selectWorkspace' : 'login')
|
||||||
if (!pages.includes(page)) {
|
if (!pages.includes(page)) {
|
||||||
page = 'login'
|
page = 'login'
|
||||||
|
@ -14,13 +14,21 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { OK, setMetadata, Severity, Status } from '@hcengineering/platform'
|
import { getMetadata, OK, setMetadata, Severity, Status } from '@hcengineering/platform'
|
||||||
import { getCurrentLocation, navigate, Location, setMetadataLocalStorage } from '@hcengineering/ui'
|
|
||||||
import presentation from '@hcengineering/presentation'
|
import presentation from '@hcengineering/presentation'
|
||||||
|
import {
|
||||||
|
fetchMetadataLocalStorage,
|
||||||
|
getCurrentLocation,
|
||||||
|
Location,
|
||||||
|
navigate,
|
||||||
|
setMetadataLocalStorage,
|
||||||
|
ticker
|
||||||
|
} from '@hcengineering/ui'
|
||||||
|
|
||||||
import { doLogin, getWorkspaces, navigateToWorkspace, selectWorkspace } from '../utils'
|
import { doLogin, getAccount, getWorkspaces, navigateToWorkspace, selectWorkspace } from '../utils'
|
||||||
import Form from './Form.svelte'
|
import Form from './Form.svelte'
|
||||||
|
|
||||||
|
import { LoginInfo } from '@hcengineering/login'
|
||||||
import login from '../plugin'
|
import login from '../plugin'
|
||||||
|
|
||||||
export let navigateUrl: string | undefined = undefined
|
export let navigateUrl: string | undefined = undefined
|
||||||
@ -40,18 +48,17 @@
|
|||||||
password: ''
|
password: ''
|
||||||
}
|
}
|
||||||
|
|
||||||
let status = OK
|
async function doLoginNavigate (
|
||||||
|
result: LoginInfo | undefined,
|
||||||
const action = {
|
updateStatus: (status: Status<any>) => void,
|
||||||
i18n: login.string.LogIn,
|
token?: string
|
||||||
func: async () => {
|
): Promise<boolean> {
|
||||||
status = new Status(Severity.INFO, login.status.ConnectingToServer, {})
|
|
||||||
|
|
||||||
const [loginStatus, result] = await doLogin(object.username, object.password)
|
|
||||||
status = loginStatus
|
|
||||||
|
|
||||||
if (result !== undefined) {
|
if (result !== undefined) {
|
||||||
setMetadata(presentation.metadata.Token, result.token)
|
if (result.token != null && getMetadata(presentation.metadata.Token) === result.token) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
setMetadata(presentation.metadata.Token, token ?? result.token)
|
||||||
|
setMetadataLocalStorage(login.metadata.LastToken, token ?? result.token)
|
||||||
setMetadataLocalStorage(login.metadata.LoginEndpoint, result.endpoint)
|
setMetadataLocalStorage(login.metadata.LoginEndpoint, result.endpoint)
|
||||||
setMetadataLocalStorage(login.metadata.LoginEmail, result.email)
|
setMetadataLocalStorage(login.metadata.LoginEmail, result.email)
|
||||||
|
|
||||||
@ -62,12 +69,12 @@
|
|||||||
if (workspace !== undefined) {
|
if (workspace !== undefined) {
|
||||||
const workspaces = await getWorkspaces()
|
const workspaces = await getWorkspaces()
|
||||||
if (workspaces.find((p) => p.workspace === workspace) !== undefined) {
|
if (workspaces.find((p) => p.workspace === workspace) !== undefined) {
|
||||||
status = new Status(Severity.INFO, login.status.ConnectingToServer, {})
|
updateStatus(new Status(Severity.INFO, login.status.ConnectingToServer, {}))
|
||||||
|
|
||||||
const [loginStatus, result] = await selectWorkspace(workspace)
|
const [loginStatus, result] = await selectWorkspace(workspace)
|
||||||
status = loginStatus
|
updateStatus(loginStatus)
|
||||||
navigateToWorkspace(workspace, result, navigateUrl)
|
navigateToWorkspace(workspace, result, navigateUrl)
|
||||||
return
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@ -81,7 +88,22 @@
|
|||||||
loc.query = { ...loc.query, navigateUrl }
|
loc.query = { ...loc.query, navigateUrl }
|
||||||
}
|
}
|
||||||
navigate(loc)
|
navigate(loc)
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let status = OK
|
||||||
|
|
||||||
|
const action = {
|
||||||
|
i18n: login.string.LogIn,
|
||||||
|
func: async () => {
|
||||||
|
status = new Status(Severity.INFO, login.status.ConnectingToServer, {})
|
||||||
|
const [loginStatus, result] = await doLogin(object.username, object.password)
|
||||||
|
status = loginStatus
|
||||||
|
await doLoginNavigate(result, (st) => {
|
||||||
|
status = st
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,6 +117,30 @@
|
|||||||
navigate(loc)
|
navigate(loc)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function chooseToken (time: number): Promise<void> {
|
||||||
|
if (getMetadata(presentation.metadata.Token) == null) {
|
||||||
|
const lastToken = fetchMetadataLocalStorage(login.metadata.LastToken)
|
||||||
|
if (lastToken != null) {
|
||||||
|
try {
|
||||||
|
const info = await getAccount(false, lastToken)
|
||||||
|
if (info !== undefined) {
|
||||||
|
await doLoginNavigate(
|
||||||
|
info,
|
||||||
|
(st) => {
|
||||||
|
status = st
|
||||||
|
},
|
||||||
|
lastToken
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
setMetadataLocalStorage(login.metadata.LastToken, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: chooseToken($ticker)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Form
|
<Form
|
||||||
|
@ -111,8 +111,11 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form class="container" style:padding={$deviceInfo.docWidth <= 480 ? '1.25rem' : '5rem'}>
|
<form class="container h-60" style:padding={$deviceInfo.docWidth <= 480 ? '1.25rem' : '5rem'}>
|
||||||
<div class="grow-separator" />
|
<div class="grow-separator" />
|
||||||
|
<div class="fs-title">
|
||||||
|
{account?.email}
|
||||||
|
</div>
|
||||||
<div class="title"><Label label={login.string.SelectWorkspace} /></div>
|
<div class="title"><Label label={login.string.SelectWorkspace} /></div>
|
||||||
<div class="status">
|
<div class="status">
|
||||||
<StatusControl {status} />
|
<StatusControl {status} />
|
||||||
@ -126,7 +129,7 @@
|
|||||||
class="workspace flex-center fs-title cursor-pointer focused-button bordered form-row"
|
class="workspace flex-center fs-title cursor-pointer focused-button bordered form-row"
|
||||||
on:click={() => select(workspace.workspace)}
|
on:click={() => select(workspace.workspace)}
|
||||||
>
|
>
|
||||||
{workspace.workspace}
|
{workspace.workspaceName ?? workspace.workspace}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
{#if workspaces.length === 0 && account?.confirmed === true}
|
{#if workspaces.length === 0 && account?.confirmed === true}
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { OK, Severity, Status, setMetadata } from '@hcengineering/platform'
|
import { OK, Severity, Status, setMetadata } from '@hcengineering/platform'
|
||||||
import presentation from '@hcengineering/presentation'
|
import presentation from '@hcengineering/presentation'
|
||||||
import { getCurrentLocation, navigate } from '@hcengineering/ui'
|
import { getCurrentLocation, navigate, setMetadataLocalStorage } from '@hcengineering/ui'
|
||||||
import login from '../plugin'
|
import login from '../plugin'
|
||||||
import { signUp } from '../utils'
|
import { signUp } from '../utils'
|
||||||
import Form from './Form.svelte'
|
import Form from './Form.svelte'
|
||||||
@ -50,6 +50,7 @@
|
|||||||
|
|
||||||
if (result !== undefined) {
|
if (result !== undefined) {
|
||||||
setMetadata(presentation.metadata.Token, result.token)
|
setMetadata(presentation.metadata.Token, result.token)
|
||||||
|
setMetadataLocalStorage(login.metadata.LastToken, result.token)
|
||||||
const loc = getCurrentLocation()
|
const loc = getCurrentLocation()
|
||||||
loc.path[1] = 'confirmationSend'
|
loc.path[1] = 'confirmationSend'
|
||||||
loc.path.length = 2
|
loc.path.length = 2
|
||||||
|
@ -48,12 +48,6 @@ export default mergeIds(loginId, login, {
|
|||||||
WantAnotherWorkspace: '' as IntlString,
|
WantAnotherWorkspace: '' as IntlString,
|
||||||
NotSeeingWorkspace: '' as IntlString,
|
NotSeeingWorkspace: '' as IntlString,
|
||||||
ChangeAccount: '' as IntlString,
|
ChangeAccount: '' as IntlString,
|
||||||
WorkspaceNameRule: '' as IntlString,
|
|
||||||
WorkspaceNameRuleHyphen: '' as IntlString,
|
|
||||||
WorkspaceNameRuleHyphenEnd: '' as IntlString,
|
|
||||||
WorkspaceNameRuleLengthLow: '' as IntlString,
|
|
||||||
WorkspaceNameRuleLengthHigh: '' as IntlString,
|
|
||||||
WorkspaceNameRuleCapital: '' as IntlString,
|
|
||||||
ForgotPassword: '' as IntlString,
|
ForgotPassword: '' as IntlString,
|
||||||
Recover: '' as IntlString,
|
Recover: '' as IntlString,
|
||||||
KnowPassword: '' as IntlString,
|
KnowPassword: '' as IntlString,
|
||||||
|
@ -116,7 +116,9 @@ export async function signUp (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createWorkspace (workspace: string): Promise<[Status, LoginInfo | undefined]> {
|
export async function createWorkspace (
|
||||||
|
workspaceName: string
|
||||||
|
): Promise<[Status, (LoginInfo & { workspace: string }) | undefined]> {
|
||||||
const accountsUrl = getMetadata(login.metadata.AccountsUrl)
|
const accountsUrl = getMetadata(login.metadata.AccountsUrl)
|
||||||
|
|
||||||
if (accountsUrl === undefined) {
|
if (accountsUrl === undefined) {
|
||||||
@ -128,7 +130,7 @@ export async function createWorkspace (workspace: string): Promise<[Status, Logi
|
|||||||
if (overrideToken !== undefined) {
|
if (overrideToken !== undefined) {
|
||||||
const endpoint = getMetadata(login.metadata.OverrideEndpoint)
|
const endpoint = getMetadata(login.metadata.OverrideEndpoint)
|
||||||
if (endpoint !== undefined) {
|
if (endpoint !== undefined) {
|
||||||
return [OK, { token: overrideToken, endpoint, email, confirmed: true }]
|
return [OK, { token: overrideToken, endpoint, email, confirmed: true, workspace: workspaceName }]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -143,7 +145,7 @@ export async function createWorkspace (workspace: string): Promise<[Status, Logi
|
|||||||
|
|
||||||
const request = {
|
const request = {
|
||||||
method: 'createWorkspace',
|
method: 'createWorkspace',
|
||||||
params: [workspace]
|
params: [workspaceName]
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -214,7 +216,7 @@ export async function getWorkspaces (): Promise<Workspace[]> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAccount (): Promise<LoginInfo | undefined> {
|
export async function getAccount (doNavigate: boolean = true, token?: string): Promise<LoginInfo | undefined> {
|
||||||
const accountsUrl = getMetadata(login.metadata.AccountsUrl)
|
const accountsUrl = getMetadata(login.metadata.AccountsUrl)
|
||||||
|
|
||||||
if (accountsUrl === undefined) {
|
if (accountsUrl === undefined) {
|
||||||
@ -230,12 +232,14 @@ export async function getAccount (): Promise<LoginInfo | undefined> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = getMetadata(presentation.metadata.Token)
|
token = token ?? getMetadata(presentation.metadata.Token)
|
||||||
if (token === undefined) {
|
if (token === undefined) {
|
||||||
|
if (doNavigate) {
|
||||||
const loc = getCurrentLocation()
|
const loc = getCurrentLocation()
|
||||||
loc.path[1] = 'login'
|
loc.path[1] = 'login'
|
||||||
loc.path.length = 2
|
loc.path.length = 2
|
||||||
navigate(loc)
|
navigate(loc)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,7 +26,8 @@ export const loginId = 'login' as Plugin
|
|||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export interface Workspace {
|
export interface Workspace {
|
||||||
workspace: string
|
workspace: string //
|
||||||
|
workspaceName?: string // A company name
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -50,6 +51,7 @@ export default plugin(loginId, {
|
|||||||
metadata: {
|
metadata: {
|
||||||
AccountsUrl: '' as Asset,
|
AccountsUrl: '' as Asset,
|
||||||
LoginTokens: '' as Metadata<Record<string, string>>,
|
LoginTokens: '' as Metadata<Record<string, string>>,
|
||||||
|
LastToken: '' as Metadata<string>,
|
||||||
LoginEndpoint: '' as Metadata<string>,
|
LoginEndpoint: '' as Metadata<string>,
|
||||||
LoginEmail: '' as Metadata<string>,
|
LoginEmail: '' as Metadata<string>,
|
||||||
OverrideLoginToken: '' as Metadata<string>, // debug purposes
|
OverrideLoginToken: '' as Metadata<string>, // debug purposes
|
||||||
|
@ -98,6 +98,7 @@
|
|||||||
setMetadataLocalStorage(login.metadata.LoginTokens, tokens)
|
setMetadataLocalStorage(login.metadata.LoginTokens, tokens)
|
||||||
}
|
}
|
||||||
setMetadata(presentation.metadata.Token, null)
|
setMetadata(presentation.metadata.Token, null)
|
||||||
|
setMetadataLocalStorage(login.metadata.LastToken, null)
|
||||||
setMetadataLocalStorage(login.metadata.LoginEndpoint, null)
|
setMetadataLocalStorage(login.metadata.LoginEndpoint, null)
|
||||||
setMetadataLocalStorage(login.metadata.LoginEmail, null)
|
setMetadataLocalStorage(login.metadata.LoginEmail, null)
|
||||||
void closeClient()
|
void closeClient()
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
export let workspace: string
|
export let workspace: string
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="antiLogo red" class:mini>{workspace?.toUpperCase()?.[0]}</div>
|
<div class="antiLogo red" class:mini>{workspace?.toUpperCase()?.[0] ?? ''}</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.antiLogo {
|
.antiLogo {
|
||||||
|
@ -136,7 +136,7 @@
|
|||||||
<!-- <div class="drag"><Drag size={'small'} /></div> -->
|
<!-- <div class="drag"><Drag size={'small'} /></div> -->
|
||||||
<!-- <div class="logo empty" /> -->
|
<!-- <div class="logo empty" /> -->
|
||||||
<!-- <div class="flex-col flex-grow"> -->
|
<!-- <div class="flex-col flex-grow"> -->
|
||||||
<span class="label overflow-label flex-grow">{ws.workspace}</span>
|
<span class="label overflow-label flex-grow">{ws.workspaceName ?? ws.workspace}</span>
|
||||||
<!-- <span class="description overflow-label">Description</span> -->
|
<!-- <span class="description overflow-label">Description</span> -->
|
||||||
<!-- </div> -->
|
<!-- </div> -->
|
||||||
<div class="ap-check">
|
<div class="ap-check">
|
||||||
|
@ -32,10 +32,14 @@
|
|||||||
let admin = false
|
let admin = false
|
||||||
onDestroy(
|
onDestroy(
|
||||||
ticker.subscribe(() => {
|
ticker.subscribe(() => {
|
||||||
void fetch(endpoint + `/api/v1/statistics?token=${token}`, {}).then(async (json) => {
|
void fetch(endpoint + `/api/v1/statistics?token=${token}`, {})
|
||||||
|
.then(async (json) => {
|
||||||
data = await json.json()
|
data = await json.json()
|
||||||
admin = data?.admin ?? false
|
admin = data?.admin ?? false
|
||||||
})
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
const tabs: TabItem[] = [
|
const tabs: TabItem[] = [
|
||||||
|
@ -124,8 +124,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
getResource(login.function.GetWorkspaces).then(async (getWorkspaceFn) => {
|
void getResource(login.function.GetWorkspaces).then(async (getWorkspaceFn) => {
|
||||||
$workspacesStore = await getWorkspaceFn()
|
$workspacesStore = await getWorkspaceFn()
|
||||||
|
await updateWindowTitle(getLocation())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -188,8 +189,15 @@
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
let windowWorkspaceName = ''
|
||||||
|
|
||||||
async function updateWindowTitle (loc: Location): Promise<void> {
|
async function updateWindowTitle (loc: Location): Promise<void> {
|
||||||
const ws = loc.path[1]
|
let ws = loc.path[1]
|
||||||
|
const wsName = $workspacesStore.find((it) => it.workspace === ws)
|
||||||
|
if (wsName !== undefined) {
|
||||||
|
ws = wsName?.workspaceName ?? wsName.workspace
|
||||||
|
windowWorkspaceName = ws
|
||||||
|
}
|
||||||
const docTitle = await getWindowTitle(loc)
|
const docTitle = await getWindowTitle(loc)
|
||||||
if (docTitle !== undefined && docTitle !== '') {
|
if (docTitle !== undefined && docTitle !== '') {
|
||||||
document.title = ws == null ? docTitle : `${docTitle} - ${ws}`
|
document.title = ws == null ? docTitle : `${docTitle} - ${ws}`
|
||||||
@ -663,7 +671,7 @@
|
|||||||
showPopup(SelectWorkspaceMenu, {}, popupSpacePosition)
|
showPopup(SelectWorkspaceMenu, {}, popupSpacePosition)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Logo mini={appsMini} workspace={$resolvedLocationStore.path[1]} />
|
<Logo mini={appsMini} workspace={windowWorkspaceName ?? $resolvedLocationStore.path[1]} />
|
||||||
</div>
|
</div>
|
||||||
<div class="topmenu-container clear-mins flex-no-shrink" class:mini={appsMini}>
|
<div class="topmenu-container clear-mins flex-no-shrink" class:mini={appsMini}>
|
||||||
<AppItem
|
<AppItem
|
||||||
|
@ -53,7 +53,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</FixedColumn>
|
</FixedColumn>
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
{#each Object.entries(metrics.measurements) as [k, v], i}
|
{#each Object.entries(metrics.measurements) as [k, v], i (k)}
|
||||||
<div style:margin-left={`${level * 0.5}rem`}>
|
<div style:margin-left={`${level * 0.5}rem`}>
|
||||||
<svelte:self metrics={v} name="{i}. {k}" level={level + 1} />
|
<svelte:self metrics={v} name="{i}. {k}" level={level + 1} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import client from '@hcengineering/client'
|
import client from '@hcengineering/client'
|
||||||
import core, {
|
import core, {
|
||||||
type AccountClient,
|
|
||||||
type Client,
|
|
||||||
ClientConnectEvent,
|
ClientConnectEvent,
|
||||||
type Version,
|
|
||||||
getCurrentAccount,
|
getCurrentAccount,
|
||||||
setCurrentAccount,
|
setCurrentAccount,
|
||||||
versionToString
|
versionToString,
|
||||||
|
type AccountClient,
|
||||||
|
type Client,
|
||||||
|
type Version
|
||||||
} from '@hcengineering/core'
|
} from '@hcengineering/core'
|
||||||
import login, { loginId } from '@hcengineering/login'
|
import login, { loginId } from '@hcengineering/login'
|
||||||
import { addEventListener, broadcastEvent, getMetadata, getResource, setMetadata } from '@hcengineering/platform'
|
import { addEventListener, broadcastEvent, getMetadata, getResource, setMetadata } from '@hcengineering/platform'
|
||||||
@ -217,6 +217,7 @@ function clearMetadata (ws: string): void {
|
|||||||
setMetadataLocalStorage(login.metadata.LoginTokens, tokens)
|
setMetadataLocalStorage(login.metadata.LoginTokens, tokens)
|
||||||
}
|
}
|
||||||
setMetadata(presentation.metadata.Token, null)
|
setMetadata(presentation.metadata.Token, null)
|
||||||
|
setMetadataLocalStorage(login.metadata.LastToken, null)
|
||||||
document.cookie =
|
document.cookie =
|
||||||
encodeURIComponent(presentation.metadata.Token.replaceAll(':', '-')) + '=' + encodeURIComponent('') + '; path=/'
|
encodeURIComponent(presentation.metadata.Token.replaceAll(':', '-')) + '=' + encodeURIComponent('') + '; path=/'
|
||||||
setMetadataLocalStorage(login.metadata.LoginEndpoint, null)
|
setMetadataLocalStorage(login.metadata.LoginEndpoint, null)
|
||||||
|
@ -195,6 +195,7 @@ export function signOut (): void {
|
|||||||
setMetadataLocalStorage(login.metadata.LoginTokens, tokens)
|
setMetadataLocalStorage(login.metadata.LoginTokens, tokens)
|
||||||
}
|
}
|
||||||
setMetadata(presentation.metadata.Token, null)
|
setMetadata(presentation.metadata.Token, null)
|
||||||
|
setMetadataLocalStorage(login.metadata.LastToken, null)
|
||||||
setMetadataLocalStorage(login.metadata.LoginEndpoint, null)
|
setMetadataLocalStorage(login.metadata.LoginEndpoint, null)
|
||||||
setMetadataLocalStorage(login.metadata.LoginEmail, null)
|
setMetadataLocalStorage(login.metadata.LoginEmail, null)
|
||||||
void closeClient()
|
void closeClient()
|
||||||
|
@ -82,6 +82,12 @@ if (frontUrl === undefined) {
|
|||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const accountsUrl = process.env.ACCOUNTS_URL
|
||||||
|
if (accountsUrl === undefined) {
|
||||||
|
console.log('Please provide ACCOUNTS_URL url')
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
const sesUrl = process.env.SES_URL
|
const sesUrl = process.env.SES_URL
|
||||||
const cursorMaxTime = process.env.SERVER_CURSOR_MAXTIMEMS
|
const cursorMaxTime = process.env.SERVER_CURSOR_MAXTIMEMS
|
||||||
|
|
||||||
@ -105,7 +111,8 @@ const shutdown = start(url, {
|
|||||||
indexParallel: 2,
|
indexParallel: 2,
|
||||||
indexProcessing: 50,
|
indexProcessing: 50,
|
||||||
productId: '',
|
productId: '',
|
||||||
enableCompression
|
enableCompression,
|
||||||
|
accountsUrl
|
||||||
})
|
})
|
||||||
|
|
||||||
const close = (): void => {
|
const close = (): void => {
|
||||||
|
@ -188,6 +188,8 @@ export function start (
|
|||||||
indexParallel: number // 2
|
indexParallel: number // 2
|
||||||
|
|
||||||
enableCompression?: boolean
|
enableCompression?: boolean
|
||||||
|
|
||||||
|
accountsUrl: string
|
||||||
}
|
}
|
||||||
): () => Promise<void> {
|
): () => Promise<void> {
|
||||||
addLocation(serverAttachmentId, () => import('@hcengineering/server-attachment-resources'))
|
addLocation(serverAttachmentId, () => import('@hcengineering/server-attachment-resources'))
|
||||||
@ -270,7 +272,7 @@ export function start (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pipelineFactory: PipelineFactory = (ctx, workspace, upgrade, broadcast) => {
|
const pipelineFactory: PipelineFactory = (ctx, workspace, upgrade, broadcast) => {
|
||||||
const wsMetrics = metrics.newChild('🧲 ' + workspace.name, {})
|
const wsMetrics = metrics.newChild('🧲 session', {})
|
||||||
const conf: DbConfiguration = {
|
const conf: DbConfiguration = {
|
||||||
domains: {
|
domains: {
|
||||||
[DOMAIN_TX]: 'MongoTx',
|
[DOMAIN_TX]: 'MongoTx',
|
||||||
@ -357,6 +359,7 @@ export function start (
|
|||||||
port: opt.port,
|
port: opt.port,
|
||||||
productId: opt.productId,
|
productId: opt.productId,
|
||||||
serverFactory: opt.serverFactory,
|
serverFactory: opt.serverFactory,
|
||||||
enableCompression: opt.enableCompression
|
enableCompression: opt.enableCompression,
|
||||||
|
accountsUrl: opt.accountsUrl
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
cd ./dev/tool
|
cd ./dev/tool
|
||||||
rushx run-local create-workspace ws1 -o DevWorkspace # Create workspace
|
rushx run-local create-workspace ws1 -w DevWorkspace # Create workspace
|
||||||
rushx run-local create-account user1 -p 1234 -f John -l Appleseed # Create account
|
rushx run-local create-account user1 -p 1234 -f John -l Appleseed # Create account
|
||||||
rushx run-local configure ws1 --list --enable '*' # Enable all modules, even if they are not yet intended to be used by a wide audience.
|
rushx run-local configure ws1 --list --enable '*' # Enable all modules, even if they are not yet intended to be used by a wide audience.
|
||||||
rushx run-local assign-workspace user1 ws1 # Assign workspace to user.
|
rushx run-local assign-workspace user1 ws1 # Assign workspace to user.
|
||||||
|
@ -131,7 +131,7 @@ async function getUpdateBacklinksTxes (
|
|||||||
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 = getMetadata(serverCore.metadata.FrontUrl) ?? ''
|
||||||
const path = `${workbenchId}/${control.workspace.name}/${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>`
|
||||||
}
|
}
|
||||||
|
@ -134,7 +134,7 @@ 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 = getMetadata(serverCore.metadata.FrontUrl) ?? ''
|
||||||
const path = `${workbenchId}/${control.workspace.name}/${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)}</a>`
|
||||||
}
|
}
|
||||||
@ -153,7 +153,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 = getMetadata(serverCore.metadata.FrontUrl) ?? ''
|
||||||
const path = `${workbenchId}/${control.workspace.name}/${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>`
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,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 = getMetadata(serverCore.metadata.FrontUrl) ?? ''
|
||||||
const path = `${workbenchId}/${control.workspace.name}/${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 = getMetadata(serverCore.metadata.FrontUrl) ?? ''
|
||||||
const path = `${workbenchId}/${control.workspace.name}/${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>`
|
||||||
}
|
}
|
||||||
|
@ -47,7 +47,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 = getMetadata(serverCore.metadata.FrontUrl) ?? ''
|
||||||
const path = `${workbenchId}/${control.workspace.name}/${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>`
|
||||||
}
|
}
|
||||||
@ -67,7 +67,7 @@ export async function applicationHTMLPresenter (doc: Doc, control: TriggerContro
|
|||||||
const applicant = doc as Applicant
|
const applicant = doc as Applicant
|
||||||
const front = getMetadata(serverCore.metadata.FrontUrl) ?? ''
|
const front = getMetadata(serverCore.metadata.FrontUrl) ?? ''
|
||||||
const id = getSequenceId(applicant, control)
|
const id = getSequenceId(applicant, control)
|
||||||
const path = `${workbenchId}/${control.workspace.name}/${recruitId}/${id}`
|
const path = `${workbenchId}/${control.workspace.workspaceUrl}/${recruitId}/${id}`
|
||||||
const link = concatLink(front, path)
|
const link = concatLink(front, path)
|
||||||
return `<a href="${link}">${id}</a>`
|
return `<a href="${link}">${id}</a>`
|
||||||
}
|
}
|
||||||
|
@ -59,7 +59,7 @@ export async function issueHTMLPresenter (doc: Doc, control: TriggerControl): Pr
|
|||||||
const issue = doc as Issue
|
const issue = doc as Issue
|
||||||
const issueId = await getIssueId(issue, control)
|
const issueId = await getIssueId(issue, control)
|
||||||
const front = getMetadata(serverCore.metadata.FrontUrl) ?? ''
|
const front = getMetadata(serverCore.metadata.FrontUrl) ?? ''
|
||||||
const path = `${workbenchId}/${control.workspace.name}/${trackerId}/${issueId}`
|
const path = `${workbenchId}/${control.workspace.workspaceUrl}/${trackerId}/${issueId}`
|
||||||
const link = concatLink(front, path)
|
const link = concatLink(front, path)
|
||||||
return `<a href="${link}">${issueId}</a> ${issue.title}`
|
return `<a href="${link}">${issueId}</a> ${issue.title}`
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
import builder, { migrateOperations, getModelVersion } from '@hcengineering/model-all'
|
import builder, { migrateOperations, getModelVersion } from '@hcengineering/model-all'
|
||||||
import { randomBytes } from 'crypto'
|
import { randomBytes } from 'crypto'
|
||||||
import { Db, MongoClient } from 'mongodb'
|
import { Db, MongoClient } from 'mongodb'
|
||||||
import accountPlugin, { getAccount, getMethods, getWorkspace } from '..'
|
import accountPlugin, { getAccount, getMethods, getWorkspaceByUrl } from '..'
|
||||||
import { setMetadata } from '@hcengineering/platform'
|
import { setMetadata } from '@hcengineering/platform'
|
||||||
|
|
||||||
const DB_NAME = 'test_accounts'
|
const DB_NAME = 'test_accounts'
|
||||||
@ -147,14 +147,14 @@ describe('server', () => {
|
|||||||
|
|
||||||
// Check we had one
|
// Check we had one
|
||||||
expect((await getAccount(db, 'andrey'))?.workspaces.length).toEqual(1)
|
expect((await getAccount(db, 'andrey'))?.workspaces.length).toEqual(1)
|
||||||
expect((await getWorkspace(db, '', workspace))?.accounts.length).toEqual(1)
|
expect((await getWorkspaceByUrl(db, '', workspace))?.accounts.length).toEqual(1)
|
||||||
|
|
||||||
await methods.removeWorkspace(db, '', {
|
await methods.removeWorkspace(db, '', {
|
||||||
method: 'removeWorkspace',
|
method: 'removeWorkspace',
|
||||||
params: ['andrey', workspace]
|
params: ['andrey', workspace]
|
||||||
})
|
})
|
||||||
expect((await getAccount(db, 'andrey'))?.workspaces.length).toEqual(0)
|
expect((await getAccount(db, 'andrey'))?.workspaces.length).toEqual(0)
|
||||||
expect((await getWorkspace(db, '', workspace))?.accounts.length).toEqual(0)
|
expect((await getWorkspaceByUrl(db, '', workspace))?.accounts.length).toEqual(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
|
@ -27,6 +27,7 @@ import core, {
|
|||||||
AccountRole,
|
AccountRole,
|
||||||
concatLink,
|
concatLink,
|
||||||
Data,
|
Data,
|
||||||
|
generateId,
|
||||||
getWorkspaceId,
|
getWorkspaceId,
|
||||||
Ref,
|
Ref,
|
||||||
systemAccountEmail,
|
systemAccountEmail,
|
||||||
@ -93,12 +94,14 @@ export interface Account {
|
|||||||
*/
|
*/
|
||||||
export interface Workspace {
|
export interface Workspace {
|
||||||
_id: ObjectId
|
_id: ObjectId
|
||||||
workspace: string
|
workspace: string // An uniq workspace name, Database names
|
||||||
organisation: string
|
|
||||||
accounts: ObjectId[]
|
accounts: ObjectId[]
|
||||||
productId: string
|
productId: string
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
version?: Data<Version>
|
version?: Data<Version>
|
||||||
|
|
||||||
|
workspaceUrl?: string | null // An optional url to the workspace, if not set workspace will be used
|
||||||
|
workspaceName?: string // An displayed workspace name
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -175,13 +178,30 @@ function withProductId (productId: string, query: Filter<Workspace>): Filter<Wor
|
|||||||
}
|
}
|
||||||
: { productId, ...query }
|
: { productId, ...query }
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
* @param db -
|
||||||
|
* @param workspaceUrl -
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export async function getWorkspaceByUrl (db: Db, productId: string, workspaceUrl: string): Promise<Workspace | null> {
|
||||||
|
const res = await db.collection<Workspace>(WORKSPACE_COLLECTION).findOne(withProductId(productId, { workspaceUrl }))
|
||||||
|
if (res != null) {
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
// Fallback to old workspaces.
|
||||||
|
return await db
|
||||||
|
.collection<Workspace>(WORKSPACE_COLLECTION)
|
||||||
|
.findOne(withProductId(productId, { workspace: workspaceUrl, workspaceUrl: { $exists: false } }))
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
* @param db -
|
* @param db -
|
||||||
* @param workspace -
|
* @param workspace -
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export async function getWorkspace (db: Db, productId: string, workspace: string): Promise<Workspace | null> {
|
export async function getWorkspaceById (db: Db, productId: string, workspace: string): Promise<Workspace | null> {
|
||||||
return await db.collection<Workspace>(WORKSPACE_COLLECTION).findOne(withProductId(productId, { workspace }))
|
return await db.collection<Workspace>(WORKSPACE_COLLECTION).findOne(withProductId(productId, { workspace }))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -203,7 +223,12 @@ async function getAccountInfo (db: Db, email: string, password: string): Promise
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function getAccountInfoByToken (db: Db, productId: string, token: string): Promise<AccountInfo> {
|
async function getAccountInfoByToken (db: Db, productId: string, token: string): Promise<AccountInfo> {
|
||||||
const { email } = decodeToken(token)
|
let email: string = ''
|
||||||
|
try {
|
||||||
|
email = decodeToken(token)?.email
|
||||||
|
} catch (err: any) {
|
||||||
|
throw new PlatformError(new Status(Severity.ERROR, platform.status.Unauthorized, {}))
|
||||||
|
}
|
||||||
const account = await getAccount(db, email)
|
const account = await getAccount(db, email)
|
||||||
if (account === null) {
|
if (account === null) {
|
||||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotFound, { account: email }))
|
throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotFound, { account: email }))
|
||||||
@ -253,7 +278,7 @@ export async function selectWorkspace (
|
|||||||
db: Db,
|
db: Db,
|
||||||
productId: string,
|
productId: string,
|
||||||
token: string,
|
token: string,
|
||||||
workspace: string,
|
workspaceUrl: string,
|
||||||
allowAdmin: boolean = true
|
allowAdmin: boolean = true
|
||||||
): Promise<WorkspaceLoginInfo> {
|
): Promise<WorkspaceLoginInfo> {
|
||||||
let { email } = decodeToken(token)
|
let { email } = decodeToken(token)
|
||||||
@ -263,21 +288,25 @@ export async function selectWorkspace (
|
|||||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotFound, { account: email }))
|
throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotFound, { account: email }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const workspaceInfo = await getWorkspaceByUrl(db, productId, workspaceUrl)
|
||||||
|
if (workspaceInfo == null) {
|
||||||
|
throw new PlatformError(new Status(Severity.ERROR, platform.status.WorkspaceNotFound, { workspace: workspaceUrl }))
|
||||||
|
}
|
||||||
if (accountInfo.admin === true && allowAdmin) {
|
if (accountInfo.admin === true && allowAdmin) {
|
||||||
return {
|
return {
|
||||||
endpoint: getEndpoint(),
|
endpoint: getEndpoint(),
|
||||||
email,
|
email,
|
||||||
token: generateToken(email, getWorkspaceId(workspace, productId), getExtra(accountInfo)),
|
token: generateToken(email, getWorkspaceId(workspaceInfo.workspace, productId), getExtra(accountInfo)),
|
||||||
workspace,
|
workspace: workspaceUrl,
|
||||||
productId
|
productId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const workspaceInfo = await getWorkspace(db, productId, workspace)
|
|
||||||
|
|
||||||
if (workspaceInfo !== null) {
|
if (workspaceInfo !== null) {
|
||||||
if (workspaceInfo.disabled === true) {
|
if (workspaceInfo.disabled === true) {
|
||||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.WorkspaceNotFound, { workspace }))
|
throw new PlatformError(
|
||||||
|
new Status(Severity.ERROR, platform.status.WorkspaceNotFound, { workspace: workspaceUrl })
|
||||||
|
)
|
||||||
}
|
}
|
||||||
const workspaces = accountInfo.workspaces
|
const workspaces = accountInfo.workspaces
|
||||||
|
|
||||||
@ -286,8 +315,8 @@ export async function selectWorkspace (
|
|||||||
const result = {
|
const result = {
|
||||||
endpoint: getEndpoint(),
|
endpoint: getEndpoint(),
|
||||||
email,
|
email,
|
||||||
token: generateToken(email, getWorkspaceId(workspace, productId), getExtra(accountInfo)),
|
token: generateToken(email, getWorkspaceId(workspaceInfo.workspace, productId), getExtra(accountInfo)),
|
||||||
workspace,
|
workspace: workspaceUrl,
|
||||||
productId
|
productId
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
@ -546,11 +575,11 @@ export async function createAccount (
|
|||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export async function listWorkspaces (db: Db, productId: string): Promise<WorkspaceInfoOnly[]> {
|
export async function listWorkspaces (db: Db, productId: string): Promise<ClientWorkspaceInfo[]> {
|
||||||
return (await db.collection<Workspace>(WORKSPACE_COLLECTION).find(withProductId(productId, {})).toArray())
|
return (await db.collection<Workspace>(WORKSPACE_COLLECTION).find(withProductId(productId, {})).toArray())
|
||||||
.map((it) => ({ ...it, productId }))
|
.map((it) => ({ ...it, productId }))
|
||||||
.filter((it) => it.disabled !== true)
|
.filter((it) => it.disabled !== true)
|
||||||
.map(trimWorkspace)
|
.map(mapToClientWorkspace)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -560,6 +589,97 @@ export async function listAccounts (db: Db): Promise<Account[]> {
|
|||||||
return await db.collection<Account>(ACCOUNT_COLLECTION).find({}).toArray()
|
return await db.collection<Account>(ACCOUNT_COLLECTION).find({}).toArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const workspaceReg = /[a-z0-9]/
|
||||||
|
|
||||||
|
function stripId (name: string): string {
|
||||||
|
let workspaceId = ''
|
||||||
|
for (const c of name.toLowerCase()) {
|
||||||
|
if (workspaceReg.test(c) || c === '-') {
|
||||||
|
workspaceId += c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return workspaceId
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEmailName (email: string): string {
|
||||||
|
return email.split('@')[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateWorkspaceRecord (
|
||||||
|
db: Db,
|
||||||
|
email: string,
|
||||||
|
productId: string,
|
||||||
|
version: Data<Version>,
|
||||||
|
workspaceName: string,
|
||||||
|
fixedWorkspace?: string
|
||||||
|
): Promise<Workspace> {
|
||||||
|
const coll = db.collection<Omit<Workspace, '_id'>>(WORKSPACE_COLLECTION)
|
||||||
|
if (fixedWorkspace !== undefined) {
|
||||||
|
const ws = await coll.find<Workspace>({ workspaceUrl: fixedWorkspace }).toArray()
|
||||||
|
if ((await getWorkspaceById(db, productId, fixedWorkspace)) !== null || ws.length > 0) {
|
||||||
|
throw new PlatformError(
|
||||||
|
new Status(Severity.ERROR, platform.status.WorkspaceAlreadyExists, { workspace: fixedWorkspace })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const data = {
|
||||||
|
workspace: fixedWorkspace,
|
||||||
|
workspaceUrl: fixedWorkspace,
|
||||||
|
productId,
|
||||||
|
version,
|
||||||
|
workspaceName,
|
||||||
|
accounts: [],
|
||||||
|
disabled: false
|
||||||
|
}
|
||||||
|
// Add fixed workspace
|
||||||
|
const id = await coll.insertOne(data)
|
||||||
|
return { _id: id.insertedId, ...data }
|
||||||
|
}
|
||||||
|
const workspaceUrlPrefix = stripId(workspaceName)
|
||||||
|
const workspaceIdPrefix = stripId(getEmailName(email)).slice(0, 12) + '-' + workspaceUrlPrefix.slice(12)
|
||||||
|
let iteration = 0
|
||||||
|
let idPostfix = generateId('-')
|
||||||
|
let urlPostfix = ''
|
||||||
|
while (true) {
|
||||||
|
const workspace = 'w-' + workspaceIdPrefix + '-' + idPostfix
|
||||||
|
let workspaceUrl =
|
||||||
|
workspaceUrlPrefix + (workspaceUrlPrefix.length > 0 && urlPostfix.length > 0 ? '-' : '') + urlPostfix
|
||||||
|
if (workspaceUrl.trim().length === 0) {
|
||||||
|
workspaceUrl = generateId('-')
|
||||||
|
}
|
||||||
|
const ws = await coll.find<Workspace>({ $or: [{ workspaceUrl }, { workspace }] }).toArray()
|
||||||
|
if (ws.length === 0) {
|
||||||
|
const data = {
|
||||||
|
workspace,
|
||||||
|
workspaceUrl,
|
||||||
|
productId,
|
||||||
|
version,
|
||||||
|
workspaceName,
|
||||||
|
accounts: [],
|
||||||
|
disabled: false
|
||||||
|
}
|
||||||
|
// Nice we do not have a workspace or workspaceUrl duplicated.
|
||||||
|
const id = await coll.insertOne(data)
|
||||||
|
return { _id: id.insertedId, ...data }
|
||||||
|
}
|
||||||
|
for (const w of ws) {
|
||||||
|
if (w.workspace === workspaceUrl) {
|
||||||
|
idPostfix = generateId('-')
|
||||||
|
}
|
||||||
|
if (w.workspaceUrl === workspaceUrl) {
|
||||||
|
urlPostfix = generateId('-')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
iteration++
|
||||||
|
|
||||||
|
// A stupid check, but for sure we not hang.
|
||||||
|
if (iteration > 10000) {
|
||||||
|
throw new PlatformError(new Status(Severity.ERROR, platform.status.WorkspaceRateLimit, { workspace }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let searchPromise: Promise<Workspace> | undefined
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
@ -569,32 +689,36 @@ export async function createWorkspace (
|
|||||||
migrationOperation: [string, MigrateOperation][],
|
migrationOperation: [string, MigrateOperation][],
|
||||||
db: Db,
|
db: Db,
|
||||||
productId: string,
|
productId: string,
|
||||||
workspace: string,
|
email: string,
|
||||||
organisation: string
|
workspaceName: string,
|
||||||
): Promise<string> {
|
workspace?: string
|
||||||
if ((await getWorkspace(db, productId, workspace)) !== null) {
|
): Promise<{ workspaceInfo: Workspace, err?: any }> {
|
||||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.WorkspaceAlreadyExists, { workspace }))
|
// We need to search for duplicate workspaceUrl
|
||||||
}
|
await searchPromise
|
||||||
const result = await db
|
|
||||||
.collection(WORKSPACE_COLLECTION)
|
// Safe generate workspace record.
|
||||||
.insertOne({
|
searchPromise = generateWorkspaceRecord(db, email, productId, version, workspaceName, workspace)
|
||||||
workspace,
|
|
||||||
organisation,
|
const workspaceInfo = await searchPromise
|
||||||
version,
|
try {
|
||||||
productId
|
|
||||||
})
|
|
||||||
.then((e) => e.insertedId.toHexString())
|
|
||||||
const initWS = getMetadata(toolPlugin.metadata.InitWorkspace)
|
const initWS = getMetadata(toolPlugin.metadata.InitWorkspace)
|
||||||
|
const wsId = getWorkspaceId(workspaceInfo.workspace, productId)
|
||||||
if (initWS !== undefined) {
|
if (initWS !== undefined) {
|
||||||
if ((await getWorkspace(db, productId, initWS)) !== null) {
|
if ((await getWorkspaceById(db, productId, initWS)) !== null) {
|
||||||
await initModel(getTransactor(), getWorkspaceId(workspace, productId), txes, [])
|
await initModel(getTransactor(), wsId, txes, [])
|
||||||
await cloneWorkspace(getTransactor(), getWorkspaceId(initWS, productId), getWorkspaceId(workspace, productId))
|
await cloneWorkspace(
|
||||||
await upgradeModel(getTransactor(), getWorkspaceId(workspace, productId), txes, migrationOperation)
|
getTransactor(),
|
||||||
return result
|
getWorkspaceId(initWS, productId),
|
||||||
|
getWorkspaceId(workspaceInfo.workspace, productId)
|
||||||
|
)
|
||||||
|
await upgradeModel(getTransactor(), wsId, txes, migrationOperation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await initModel(getTransactor(), getWorkspaceId(workspace, productId), txes, migrationOperation)
|
await initModel(getTransactor(), wsId, txes, migrationOperation)
|
||||||
return result
|
} catch (err: any) {
|
||||||
|
return { workspaceInfo, err }
|
||||||
|
}
|
||||||
|
return { workspaceInfo }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -606,13 +730,13 @@ export async function upgradeWorkspace (
|
|||||||
migrationOperation: [string, MigrateOperation][],
|
migrationOperation: [string, MigrateOperation][],
|
||||||
productId: string,
|
productId: string,
|
||||||
db: Db,
|
db: Db,
|
||||||
workspace: string,
|
workspaceUrl: string,
|
||||||
logger: ModelLogger = consoleModelLogger,
|
logger: ModelLogger = consoleModelLogger,
|
||||||
forceUpdate: boolean = true
|
forceUpdate: boolean = true
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const ws = await getWorkspace(db, productId, workspace)
|
const ws = await getWorkspaceByUrl(db, productId, workspaceUrl)
|
||||||
if (ws === null) {
|
if (ws === null) {
|
||||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.WorkspaceNotFound, { workspace }))
|
throw new PlatformError(new Status(Severity.ERROR, platform.status.WorkspaceNotFound, { workspace: workspaceUrl }))
|
||||||
}
|
}
|
||||||
if (ws.productId !== productId) {
|
if (ws.productId !== productId) {
|
||||||
if (productId !== '' || ws.productId !== undefined) {
|
if (productId !== '' || ws.productId !== undefined) {
|
||||||
@ -621,7 +745,7 @@ export async function upgradeWorkspace (
|
|||||||
}
|
}
|
||||||
const versionStr = versionToString(version)
|
const versionStr = versionToString(version)
|
||||||
|
|
||||||
const currentVersion = await db.collection<Workspace>(WORKSPACE_COLLECTION).findOne({ workspace })
|
const currentVersion = await db.collection<Workspace>(WORKSPACE_COLLECTION).findOne({ workspace: workspaceUrl })
|
||||||
console.log(
|
console.log(
|
||||||
`${forceUpdate ? 'force-' : ''}upgrade from "${
|
`${forceUpdate ? 'force-' : ''}upgrade from "${
|
||||||
currentVersion?.version !== undefined ? versionToString(currentVersion.version) : ''
|
currentVersion?.version !== undefined ? versionToString(currentVersion.version) : ''
|
||||||
@ -632,12 +756,12 @@ export async function upgradeWorkspace (
|
|||||||
return versionStr
|
return versionStr
|
||||||
}
|
}
|
||||||
await db.collection(WORKSPACE_COLLECTION).updateOne(
|
await db.collection(WORKSPACE_COLLECTION).updateOne(
|
||||||
{ workspace },
|
{ workspace: workspaceUrl },
|
||||||
{
|
{
|
||||||
$set: { version }
|
$set: { version }
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
await upgradeModel(getTransactor(), getWorkspaceId(workspace, productId), txes, migrationOperation, logger)
|
await upgradeModel(getTransactor(), getWorkspaceId(workspaceUrl, productId), txes, migrationOperation, logger)
|
||||||
return versionStr
|
return versionStr
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -646,14 +770,12 @@ export async function upgradeWorkspace (
|
|||||||
*/
|
*/
|
||||||
export const createUserWorkspace =
|
export const createUserWorkspace =
|
||||||
(version: Data<Version>, txes: Tx[], migrationOperation: [string, MigrateOperation][]) =>
|
(version: Data<Version>, txes: Tx[], migrationOperation: [string, MigrateOperation][]) =>
|
||||||
async (db: Db, productId: string, token: string, workspace: string): Promise<LoginInfo> => {
|
async (db: Db, productId: string, token: string, workspaceName: string): Promise<LoginInfo> => {
|
||||||
if (!/^[0-9a-z][0-9a-z-]{2,62}[0-9a-z]$/.test(workspace)) {
|
|
||||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.InvalidId, { id: workspace }))
|
|
||||||
}
|
|
||||||
|
|
||||||
const { email, extra } = decodeToken(token)
|
const { email, extra } = decodeToken(token)
|
||||||
const nonConfirmed = extra?.confirmed === false
|
const nonConfirmed = extra?.confirmed === false
|
||||||
console.log(`Creating workspace ${workspace} for ${email} ${nonConfirmed ? 'non confirmed' : 'confirmed'}`)
|
console.log(
|
||||||
|
`Creating workspace for "${workspaceName}" for ${email} ${nonConfirmed ? 'non confirmed' : 'confirmed'}`
|
||||||
|
)
|
||||||
|
|
||||||
if (nonConfirmed) {
|
if (nonConfirmed) {
|
||||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotFound, { account: email }))
|
throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotFound, { account: email }))
|
||||||
@ -663,27 +785,31 @@ export const createUserWorkspace =
|
|||||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotFound, { account: email }))
|
throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotFound, { account: email }))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (info.lastWorkspace !== undefined) {
|
if (info.lastWorkspace !== undefined && info.admin === false) {
|
||||||
if (Date.now() - info.lastWorkspace < 60 * 1000) {
|
if (Date.now() - info.lastWorkspace < 60 * 1000) {
|
||||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.WorkspaceRateLimit, { workspace }))
|
throw new PlatformError(
|
||||||
|
new Status(Severity.ERROR, platform.status.WorkspaceRateLimit, { workspace: workspaceName })
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((await getWorkspace(db, productId, workspace)) !== null) {
|
const { workspaceInfo, err } = await createWorkspace(
|
||||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.WorkspaceAlreadyExists, { workspace }))
|
version,
|
||||||
}
|
txes,
|
||||||
try {
|
migrationOperation,
|
||||||
await createWorkspace(version, txes, migrationOperation, db, productId, workspace, '')
|
db,
|
||||||
} catch (err: any) {
|
productId,
|
||||||
|
email,
|
||||||
|
workspaceName
|
||||||
|
)
|
||||||
|
|
||||||
|
if (err != null) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
// We need to drop workspace, to prevent wrong data usage.
|
// We need to drop workspace, to prevent wrong data usage.
|
||||||
const ws = await getWorkspace(db, productId, workspace)
|
|
||||||
if (ws === null) {
|
|
||||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.WorkspaceNotFound, { workspace }))
|
|
||||||
}
|
|
||||||
await db.collection(WORKSPACE_COLLECTION).updateOne(
|
await db.collection(WORKSPACE_COLLECTION).updateOne(
|
||||||
{
|
{
|
||||||
_id: ws._id
|
_id: workspaceInfo._id
|
||||||
},
|
},
|
||||||
{ $set: { disabled: true, message: JSON.stringify(err?.message ?? ''), err: JSON.stringify(err) } }
|
{ $set: { disabled: true, message: JSON.stringify(err?.message ?? ''), err: JSON.stringify(err) } }
|
||||||
)
|
)
|
||||||
@ -695,16 +821,17 @@ export const createUserWorkspace =
|
|||||||
await db.collection(ACCOUNT_COLLECTION).updateOne({ _id: info._id }, { $set: { lastWorkspace: Date.now() } })
|
await db.collection(ACCOUNT_COLLECTION).updateOne({ _id: info._id }, { $set: { lastWorkspace: Date.now() } })
|
||||||
|
|
||||||
const initWS = getMetadata(toolPlugin.metadata.InitWorkspace)
|
const initWS = getMetadata(toolPlugin.metadata.InitWorkspace)
|
||||||
const shouldUpdateAccount = initWS !== undefined && (await getWorkspace(db, productId, initWS)) !== null
|
const shouldUpdateAccount = initWS !== undefined && (await getWorkspaceById(db, productId, initWS)) !== null
|
||||||
await assignWorkspace(db, productId, email, workspace, shouldUpdateAccount)
|
await assignWorkspace(db, productId, email, workspaceInfo.workspace, shouldUpdateAccount)
|
||||||
await setRole(email, workspace, productId, AccountRole.Owner)
|
await setRole(email, workspaceInfo.workspace, productId, AccountRole.Owner)
|
||||||
const result = {
|
const result = {
|
||||||
endpoint: getEndpoint(),
|
endpoint: getEndpoint(),
|
||||||
email,
|
email,
|
||||||
token: generateToken(email, getWorkspaceId(workspace, productId), getExtra(info)),
|
token: generateToken(email, getWorkspaceId(workspaceInfo.workspace, productId), getExtra(info)),
|
||||||
productId
|
productId,
|
||||||
|
workspace: workspaceInfo.workspaceUrl
|
||||||
}
|
}
|
||||||
console.log(`Creating workspace ${workspace} Done`)
|
console.log(`Creating workspace "${workspaceName}" Done`)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -720,7 +847,7 @@ export async function getInviteLink (
|
|||||||
limit: number
|
limit: number
|
||||||
): Promise<ObjectId> {
|
): Promise<ObjectId> {
|
||||||
const { workspace } = decodeToken(token)
|
const { workspace } = decodeToken(token)
|
||||||
const wsPromise = await getWorkspace(db, productId, workspace.name)
|
const wsPromise = await getWorkspaceById(db, productId, workspace.name)
|
||||||
if (wsPromise === null) {
|
if (wsPromise === null) {
|
||||||
throw new PlatformError(
|
throw new PlatformError(
|
||||||
new Status(Severity.ERROR, platform.status.WorkspaceNotFound, { workspace: workspace.name })
|
new Status(Severity.ERROR, platform.status.WorkspaceNotFound, { workspace: workspace.name })
|
||||||
@ -738,17 +865,17 @@ export async function getInviteLink (
|
|||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export type WorkspaceInfoOnly = Omit<Workspace, '_id' | 'accounts'>
|
export type ClientWorkspaceInfo = Omit<Workspace, '_id' | 'accounts' | 'workspaceUrl'>
|
||||||
|
|
||||||
function trimWorkspace (ws: Workspace): WorkspaceInfoOnly {
|
function mapToClientWorkspace (ws: Workspace): ClientWorkspaceInfo {
|
||||||
const { _id, accounts, ...data } = ws
|
const { _id, accounts, ...data } = ws
|
||||||
return data
|
return { ...data, workspace: ws.workspaceUrl ?? ws.workspace }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export async function getUserWorkspaces (db: Db, productId: string, token: string): Promise<WorkspaceInfoOnly[]> {
|
export async function getUserWorkspaces (db: Db, productId: string, token: string): Promise<ClientWorkspaceInfo[]> {
|
||||||
const { email } = decodeToken(token)
|
const { email } = decodeToken(token)
|
||||||
const account = await getAccount(db, email)
|
const account = await getAccount(db, email)
|
||||||
if (account === null) return []
|
if (account === null) return []
|
||||||
@ -759,19 +886,49 @@ export async function getUserWorkspaces (db: Db, productId: string, token: strin
|
|||||||
.toArray()
|
.toArray()
|
||||||
)
|
)
|
||||||
.filter((it) => it.disabled !== true)
|
.filter((it) => it.disabled !== true)
|
||||||
.map(trimWorkspace)
|
.map(mapToClientWorkspace)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export async function getWorkspaceInfo (db: Db, productId: string, token: string): Promise<ClientWorkspaceInfo> {
|
||||||
|
const { email, workspace } = decodeToken(token)
|
||||||
|
let account: Pick<Account, 'admin' | 'workspaces'> | null = null
|
||||||
|
if (email !== systemAccountEmail) {
|
||||||
|
account = await getAccount(db, email)
|
||||||
|
if (account === null) {
|
||||||
|
throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
account = {
|
||||||
|
admin: true,
|
||||||
|
workspaces: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [ws] = (
|
||||||
|
await db
|
||||||
|
.collection<Workspace>(WORKSPACE_COLLECTION)
|
||||||
|
.find(withProductId(productId, account.admin === true ? {} : { _id: { $in: account.workspaces } }))
|
||||||
|
.toArray()
|
||||||
|
).filter((it) => it.disabled !== true && it.workspace === workspace.name)
|
||||||
|
if (ws == null) {
|
||||||
|
throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
|
||||||
|
}
|
||||||
|
return mapToClientWorkspace(ws)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getWorkspaceAndAccount (
|
async function getWorkspaceAndAccount (
|
||||||
db: Db,
|
db: Db,
|
||||||
productId: string,
|
productId: string,
|
||||||
_email: string,
|
_email: string,
|
||||||
workspace: string
|
workspaceUrl: string
|
||||||
): Promise<{ accountId: ObjectId, workspaceId: ObjectId }> {
|
): Promise<{ accountId: ObjectId, workspaceId: ObjectId }> {
|
||||||
const email = cleanEmail(_email)
|
const email = cleanEmail(_email)
|
||||||
const wsPromise = await getWorkspace(db, productId, workspace)
|
const wsPromise = await getWorkspaceById(db, productId, workspaceUrl)
|
||||||
if (wsPromise === null) {
|
if (wsPromise === null) {
|
||||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.WorkspaceNotFound, { workspace }))
|
throw new PlatformError(new Status(Severity.ERROR, platform.status.WorkspaceNotFound, { workspace: workspaceUrl }))
|
||||||
}
|
}
|
||||||
const workspaceId = wsPromise._id
|
const workspaceId = wsPromise._id
|
||||||
const account = await getAccount(db, email)
|
const account = await getAccount(db, email)
|
||||||
@ -787,7 +944,7 @@ async function getWorkspaceAndAccount (
|
|||||||
*/
|
*/
|
||||||
export async function setRole (_email: string, workspace: string, productId: string, role: AccountRole): Promise<void> {
|
export async function setRole (_email: string, workspace: string, productId: string, role: AccountRole): Promise<void> {
|
||||||
const email = cleanEmail(_email)
|
const email = cleanEmail(_email)
|
||||||
const connection = await connect(getTransactor(), getWorkspaceId(workspace, productId), email)
|
const connection = await connect(getTransactor(), getWorkspaceId(workspace, productId))
|
||||||
try {
|
try {
|
||||||
const ops = new TxOperations(connection, core.account.System)
|
const ops = new TxOperations(connection, core.account.System)
|
||||||
|
|
||||||
@ -811,24 +968,30 @@ export async function assignWorkspace (
|
|||||||
db: Db,
|
db: Db,
|
||||||
productId: string,
|
productId: string,
|
||||||
_email: string,
|
_email: string,
|
||||||
workspace: string,
|
workspaceId: string,
|
||||||
shouldReplaceAccount: boolean = false
|
shouldReplaceAccount: boolean = false
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const email = cleanEmail(_email)
|
const email = cleanEmail(_email)
|
||||||
const initWS = getMetadata(toolPlugin.metadata.InitWorkspace)
|
const initWS = getMetadata(toolPlugin.metadata.InitWorkspace)
|
||||||
if (initWS !== undefined && initWS === workspace) {
|
if (initWS !== undefined && initWS === workspaceId) {
|
||||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
|
throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
|
||||||
}
|
}
|
||||||
const { workspaceId, accountId } = await getWorkspaceAndAccount(db, productId, email, workspace)
|
const workspaceInfo = await getWorkspaceAndAccount(db, productId, email, workspaceId)
|
||||||
const account = await db.collection<Account>(ACCOUNT_COLLECTION).findOne({ _id: accountId })
|
const account = await db.collection<Account>(ACCOUNT_COLLECTION).findOne({ _id: accountId })
|
||||||
|
|
||||||
if (account !== null) await createPersonAccount(account, productId, workspace, shouldReplaceAccount)
|
if (account !== null) {
|
||||||
|
await createPersonAccount(account, productId, workspaceId, shouldReplaceAccount)
|
||||||
|
}
|
||||||
|
|
||||||
// Add account into workspace.
|
// Add account into workspace.
|
||||||
await db.collection(WORKSPACE_COLLECTION).updateOne({ _id: workspaceId }, { $addToSet: { accounts: accountId } })
|
await db
|
||||||
|
.collection(WORKSPACE_COLLECTION)
|
||||||
|
.updateOne({ _id: workspaceInfo.workspaceId }, { $addToSet: { accounts: workspaceInfo.accountId } })
|
||||||
|
|
||||||
// Add workspace to account
|
// Add workspace to account
|
||||||
await db.collection(ACCOUNT_COLLECTION).updateOne({ _id: accountId }, { $addToSet: { workspaces: workspaceId } })
|
await db
|
||||||
|
.collection(ACCOUNT_COLLECTION)
|
||||||
|
.updateOne({ _id: workspaceInfo.accountId }, { $addToSet: { workspaces: workspaceInfo.workspaceId } })
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createEmployee (ops: TxOperations, name: string, _email: string): Promise<Ref<Person>> {
|
async function createEmployee (ops: TxOperations, name: string, _email: string): Promise<Ref<Person>> {
|
||||||
@ -1090,10 +1253,10 @@ export async function checkJoin (
|
|||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export async function dropWorkspace (db: Db, productId: string, workspace: string): Promise<void> {
|
export async function dropWorkspace (db: Db, productId: string, workspaceId: string): Promise<void> {
|
||||||
const ws = await getWorkspace(db, productId, workspace)
|
const ws = await getWorkspaceById(db, productId, workspaceId)
|
||||||
if (ws === null) {
|
if (ws === null) {
|
||||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.WorkspaceNotFound, { workspace }))
|
throw new PlatformError(new Status(Severity.ERROR, platform.status.WorkspaceNotFound, { workspace: workspaceId }))
|
||||||
}
|
}
|
||||||
await db.collection(WORKSPACE_COLLECTION).deleteOne({ _id: ws._id })
|
await db.collection(WORKSPACE_COLLECTION).deleteOne({ _id: ws._id })
|
||||||
await db
|
await db
|
||||||
@ -1138,7 +1301,7 @@ export async function leaveWorkspace (db: Db, productId: string, token: string,
|
|||||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotFound, { account: tokenData.email }))
|
throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotFound, { account: tokenData.email }))
|
||||||
}
|
}
|
||||||
|
|
||||||
const workspace = await getWorkspace(db, productId, tokenData.workspace.name)
|
const workspace = await getWorkspaceById(db, productId, tokenData.workspace.name)
|
||||||
if (workspace === null) {
|
if (workspace === null) {
|
||||||
throw new PlatformError(
|
throw new PlatformError(
|
||||||
new Status(Severity.ERROR, platform.status.WorkspaceNotFound, { workspace: tokenData.workspace.name })
|
new Status(Severity.ERROR, platform.status.WorkspaceNotFound, { workspace: tokenData.workspace.name })
|
||||||
@ -1168,7 +1331,7 @@ export async function sendInvite (db: Db, productId: string, token: string, emai
|
|||||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotFound, { account: tokenData.email }))
|
throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotFound, { account: tokenData.email }))
|
||||||
}
|
}
|
||||||
|
|
||||||
const workspace = await getWorkspace(db, productId, tokenData.workspace.name)
|
const workspace = await getWorkspaceById(db, productId, tokenData.workspace.name)
|
||||||
if (workspace === null) {
|
if (workspace === null) {
|
||||||
throw new PlatformError(
|
throw new PlatformError(
|
||||||
new Status(Severity.ERROR, platform.status.WorkspaceNotFound, { workspace: tokenData.workspace.name })
|
new Status(Severity.ERROR, platform.status.WorkspaceNotFound, { workspace: tokenData.workspace.name })
|
||||||
@ -1214,7 +1377,7 @@ export async function sendInvite (db: Db, productId: string, token: string, emai
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function deactivatePersonAccount (email: string, workspace: string, productId: string): Promise<void> {
|
async function deactivatePersonAccount (email: string, workspace: string, productId: string): Promise<void> {
|
||||||
const connection = await connect(getTransactor(), getWorkspaceId(workspace, productId), email)
|
const connection = await connect(getTransactor(), getWorkspaceId(workspace, productId))
|
||||||
try {
|
try {
|
||||||
const ops = new TxOperations(connection, core.account.System)
|
const ops = new TxOperations(connection, core.account.System)
|
||||||
|
|
||||||
@ -1244,12 +1407,17 @@ function wrap (f: (db: Db, productId: string, ...args: any[]) => Promise<any>):
|
|||||||
return await f(db, productId, ...request.params)
|
return await f(db, productId, ...request.params)
|
||||||
.then((result) => ({ id: request.id, result }))
|
.then((result) => ({ id: request.id, result }))
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error(err)
|
const status =
|
||||||
return {
|
|
||||||
error:
|
|
||||||
err instanceof PlatformError
|
err instanceof PlatformError
|
||||||
? err.status
|
? err.status
|
||||||
: new Status(Severity.ERROR, platform.status.InternalServerError, {})
|
: new Status(Severity.ERROR, platform.status.InternalServerError, {})
|
||||||
|
if (status.code === platform.status.InternalServerError) {
|
||||||
|
console.error(status, err)
|
||||||
|
} else {
|
||||||
|
console.error(status)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
error: status
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -1272,6 +1440,7 @@ export function getMethods (
|
|||||||
getUserWorkspaces: wrap(getUserWorkspaces),
|
getUserWorkspaces: wrap(getUserWorkspaces),
|
||||||
getInviteLink: wrap(getInviteLink),
|
getInviteLink: wrap(getInviteLink),
|
||||||
getAccountInfo: wrap(getAccountInfo),
|
getAccountInfo: wrap(getAccountInfo),
|
||||||
|
getWorkspaceInfo: wrap(getWorkspaceInfo),
|
||||||
createAccount: wrap(createAccount),
|
createAccount: wrap(createAccount),
|
||||||
createWorkspace: wrap(createUserWorkspace(version, txes, migrateOperations)),
|
createWorkspace: wrap(createUserWorkspace(version, txes, migrateOperations)),
|
||||||
assignWorkspace: wrap(assignWorkspace),
|
assignWorkspace: wrap(assignWorkspace),
|
||||||
|
@ -54,6 +54,7 @@ import core, {
|
|||||||
TxWorkspaceEvent,
|
TxWorkspaceEvent,
|
||||||
WorkspaceEvent,
|
WorkspaceEvent,
|
||||||
WorkspaceId,
|
WorkspaceId,
|
||||||
|
WorkspaceIdWithUrl,
|
||||||
generateId
|
generateId
|
||||||
} from '@hcengineering/core'
|
} from '@hcengineering/core'
|
||||||
import { MinioService } from '@hcengineering/minio'
|
import { MinioService } from '@hcengineering/minio'
|
||||||
@ -92,7 +93,7 @@ export interface DbConfiguration {
|
|||||||
adapters: Record<string, DbAdapterConfiguration>
|
adapters: Record<string, DbAdapterConfiguration>
|
||||||
domains: Record<string, string>
|
domains: Record<string, string>
|
||||||
defaultAdapter: string
|
defaultAdapter: string
|
||||||
workspace: WorkspaceId
|
workspace: WorkspaceIdWithUrl
|
||||||
metrics: MeasureContext
|
metrics: MeasureContext
|
||||||
fulltextAdapter: {
|
fulltextAdapter: {
|
||||||
factory: FullTextAdapterFactory
|
factory: FullTextAdapterFactory
|
||||||
@ -121,7 +122,7 @@ class TServerStorage implements ServerStorage {
|
|||||||
private readonly fulltextAdapter: FullTextAdapter,
|
private readonly fulltextAdapter: FullTextAdapter,
|
||||||
readonly storageAdapter: MinioService | undefined,
|
readonly storageAdapter: MinioService | undefined,
|
||||||
readonly modelDb: ModelDb,
|
readonly modelDb: ModelDb,
|
||||||
private readonly workspace: WorkspaceId,
|
private readonly workspace: WorkspaceIdWithUrl,
|
||||||
readonly indexFactory: (storage: ServerStorage) => FullTextIndex,
|
readonly indexFactory: (storage: ServerStorage) => FullTextIndex,
|
||||||
readonly options: ServerStorageOptions,
|
readonly options: ServerStorageOptions,
|
||||||
metrics: MeasureContext,
|
metrics: MeasureContext,
|
||||||
|
@ -37,7 +37,8 @@ import {
|
|||||||
Tx,
|
Tx,
|
||||||
TxFactory,
|
TxFactory,
|
||||||
TxResult,
|
TxResult,
|
||||||
WorkspaceId
|
WorkspaceId,
|
||||||
|
WorkspaceIdWithUrl
|
||||||
} from '@hcengineering/core'
|
} from '@hcengineering/core'
|
||||||
import { MinioService } from '@hcengineering/minio'
|
import { MinioService } from '@hcengineering/minio'
|
||||||
import type { Asset, Resource } from '@hcengineering/platform'
|
import type { Asset, Resource } from '@hcengineering/platform'
|
||||||
@ -114,7 +115,7 @@ export interface Pipeline extends LowLevelStorage {
|
|||||||
*/
|
*/
|
||||||
export interface TriggerControl {
|
export interface TriggerControl {
|
||||||
ctx: MeasureContext
|
ctx: MeasureContext
|
||||||
workspace: WorkspaceId
|
workspace: WorkspaceIdWithUrl
|
||||||
txFactory: TxFactory
|
txFactory: TxFactory
|
||||||
findAll: Storage['findAll']
|
findAll: Storage['findAll']
|
||||||
findAllCtx: <T extends Doc>(
|
findAllCtx: <T extends Doc>(
|
||||||
|
@ -157,7 +157,7 @@ describe('mongo operations', () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
defaultContentAdapter: 'default',
|
defaultContentAdapter: 'default',
|
||||||
workspace: getWorkspaceId(dbId, ''),
|
workspace: { ...getWorkspaceId(dbId, ''), workspaceName: '', workspaceUrl: '' },
|
||||||
storageFactory: () => createNullStorageFactory()
|
storageFactory: () => createNullStorageFactory()
|
||||||
}
|
}
|
||||||
const ctx = new MeasureMetricsContext('client', {})
|
const ctx = new MeasureMetricsContext('client', {})
|
||||||
|
@ -88,7 +88,8 @@ describe('server', () => {
|
|||||||
sessionFactory: (token, pipeline, broadcast) => new ClientSession(broadcast, token, pipeline),
|
sessionFactory: (token, pipeline, broadcast) => new ClientSession(broadcast, token, pipeline),
|
||||||
port: 3335,
|
port: 3335,
|
||||||
productId: '',
|
productId: '',
|
||||||
serverFactory: startHttpServer
|
serverFactory: startHttpServer,
|
||||||
|
accountsUrl: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
function connect (): WebSocket {
|
function connect (): WebSocket {
|
||||||
@ -186,7 +187,8 @@ describe('server', () => {
|
|||||||
sessionFactory: (token, pipeline, broadcast) => new ClientSession(broadcast, token, pipeline),
|
sessionFactory: (token, pipeline, broadcast) => new ClientSession(broadcast, token, pipeline),
|
||||||
port: 3336,
|
port: 3336,
|
||||||
productId: '',
|
productId: '',
|
||||||
serverFactory: startHttpServer
|
serverFactory: startHttpServer,
|
||||||
|
accountsUrl: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
async function findClose (token: string, timeoutPromise: Promise<void>, code: number): Promise<string> {
|
async function findClose (token: string, timeoutPromise: Promise<void>, code: number): Promise<string> {
|
||||||
|
@ -29,7 +29,6 @@ import { unknownError } from '@hcengineering/platform'
|
|||||||
import { readRequest, type HelloRequest, type HelloResponse, type Request, type Response } from '@hcengineering/rpc'
|
import { readRequest, type HelloRequest, type HelloResponse, type Request, type Response } from '@hcengineering/rpc'
|
||||||
import type { Pipeline, SessionContext } from '@hcengineering/server-core'
|
import type { Pipeline, SessionContext } from '@hcengineering/server-core'
|
||||||
import { type Token } from '@hcengineering/server-token'
|
import { type Token } from '@hcengineering/server-token'
|
||||||
// import WebSocket, { RawData } from 'ws'
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
LOGGING_ENABLED,
|
LOGGING_ENABLED,
|
||||||
@ -42,6 +41,11 @@ import {
|
|||||||
type Workspace
|
type Workspace
|
||||||
} from './types'
|
} from './types'
|
||||||
|
|
||||||
|
interface WorkspaceLoginInfo {
|
||||||
|
workspaceName?: string // A company name
|
||||||
|
workspace: string
|
||||||
|
}
|
||||||
|
|
||||||
function timeoutPromise (time: number): Promise<void> {
|
function timeoutPromise (time: number): Promise<void> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
setTimeout(resolve, time)
|
setTimeout(resolve, time)
|
||||||
@ -154,45 +158,96 @@ class TSessionManager implements SessionManager {
|
|||||||
return this.sessionFactory(token, pipeline, this.broadcast.bind(this))
|
return this.sessionFactory(token, pipeline, this.broadcast.bind(this))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getWorkspaceInfo (
|
||||||
|
accounts: string,
|
||||||
|
token: string
|
||||||
|
): Promise<{
|
||||||
|
workspace: string
|
||||||
|
workspaceUrl?: string | null
|
||||||
|
workspaceName?: string
|
||||||
|
}> {
|
||||||
|
const userInfo = await (
|
||||||
|
await fetch(accounts, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: 'Bearer ' + token,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
method: 'getWorkspaceInfo',
|
||||||
|
params: []
|
||||||
|
})
|
||||||
|
})
|
||||||
|
).json()
|
||||||
|
|
||||||
|
return userInfo.result as WorkspaceLoginInfo
|
||||||
|
}
|
||||||
|
|
||||||
async addSession (
|
async addSession (
|
||||||
baseCtx: MeasureContext,
|
baseCtx: MeasureContext,
|
||||||
ws: ConnectionSocket,
|
ws: ConnectionSocket,
|
||||||
token: Token,
|
token: Token,
|
||||||
|
rawToken: string,
|
||||||
pipelineFactory: PipelineFactory,
|
pipelineFactory: PipelineFactory,
|
||||||
productId: string,
|
productId: string,
|
||||||
sessionId?: string
|
sessionId: string | undefined,
|
||||||
): Promise<{ session: Session, context: MeasureContext } | { upgrade: true }> {
|
accountsUrl: string
|
||||||
|
): Promise<
|
||||||
|
{ session: Session, context: MeasureContext, workspaceName: string } | { upgrade: true } | { error: any }
|
||||||
|
> {
|
||||||
return await baseCtx.with('📲 add-session', {}, async (ctx) => {
|
return await baseCtx.with('📲 add-session', {}, async (ctx) => {
|
||||||
const wsString = toWorkspaceString(token.workspace, '@')
|
const wsString = toWorkspaceString(token.workspace, '@')
|
||||||
|
|
||||||
|
const workspaceInfo =
|
||||||
|
accountsUrl !== ''
|
||||||
|
? await this.getWorkspaceInfo(accountsUrl, rawToken)
|
||||||
|
: {
|
||||||
|
workspace: token.workspace.name,
|
||||||
|
workspaceUrl: token.workspace.name,
|
||||||
|
workspaceName: token.workspace.name
|
||||||
|
}
|
||||||
|
if (workspaceInfo === undefined) {
|
||||||
|
// No access to workspace for token.
|
||||||
|
return { error: new Error(`No access to workspace for token ${token.email} ${token.workspace.name}`) }
|
||||||
|
}
|
||||||
|
|
||||||
let workspace = this.workspaces.get(wsString)
|
let workspace = this.workspaces.get(wsString)
|
||||||
await workspace?.closing
|
await workspace?.closing
|
||||||
workspace = this.workspaces.get(wsString)
|
workspace = this.workspaces.get(wsString)
|
||||||
|
const workspaceName = workspaceInfo.workspaceName ?? workspaceInfo.workspaceUrl ?? workspaceInfo.workspace
|
||||||
|
|
||||||
if (workspace === undefined) {
|
if (workspace === undefined) {
|
||||||
workspace = this.createWorkspace(baseCtx, pipelineFactory, token)
|
workspace = this.createWorkspace(
|
||||||
|
baseCtx,
|
||||||
|
pipelineFactory,
|
||||||
|
token,
|
||||||
|
workspaceInfo.workspaceUrl ?? workspaceInfo.workspace,
|
||||||
|
workspaceName
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
let pipeline: Pipeline
|
let pipeline: Pipeline
|
||||||
if (token.extra?.model === 'upgrade') {
|
if (token.extra?.model === 'upgrade') {
|
||||||
if (workspace.upgrade) {
|
if (workspace.upgrade) {
|
||||||
pipeline = await ctx.with(
|
pipeline = await ctx.with('💤 wait ' + workspaceName, {}, async () => await (workspace as Workspace).pipeline)
|
||||||
'💤 wait ' + token.workspace.name,
|
|
||||||
{},
|
|
||||||
async () => await (workspace as Workspace).pipeline
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
pipeline = await this.createUpgradeSession(token, sessionId, ctx, wsString, workspace, pipelineFactory, ws)
|
pipeline = await this.createUpgradeSession(
|
||||||
|
token,
|
||||||
|
sessionId,
|
||||||
|
ctx,
|
||||||
|
wsString,
|
||||||
|
workspace,
|
||||||
|
pipelineFactory,
|
||||||
|
ws,
|
||||||
|
workspaceInfo.workspaceUrl ?? workspaceInfo.workspace,
|
||||||
|
workspaceName
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (workspace.upgrade) {
|
if (workspace.upgrade) {
|
||||||
return { upgrade: true }
|
return { upgrade: true }
|
||||||
}
|
}
|
||||||
pipeline = await ctx.with(
|
pipeline = await ctx.with('💤 wait ' + workspaceName, {}, async () => await (workspace as Workspace).pipeline)
|
||||||
'💤 wait ' + token.workspace.name,
|
|
||||||
{},
|
|
||||||
async () => await (workspace as Workspace).pipeline
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const session = this.createSession(token, pipeline)
|
const session = this.createSession(token, pipeline)
|
||||||
@ -211,7 +266,7 @@ class TSessionManager implements SessionManager {
|
|||||||
session.useCompression
|
session.useCompression
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return { session, context: workspace.context }
|
return { session, context: workspace.context, workspaceName }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -222,15 +277,18 @@ class TSessionManager implements SessionManager {
|
|||||||
wsString: string,
|
wsString: string,
|
||||||
workspace: Workspace,
|
workspace: Workspace,
|
||||||
pipelineFactory: PipelineFactory,
|
pipelineFactory: PipelineFactory,
|
||||||
ws: ConnectionSocket
|
ws: ConnectionSocket,
|
||||||
|
workspaceUrl: string,
|
||||||
|
workspaceName: string
|
||||||
): Promise<Pipeline> {
|
): Promise<Pipeline> {
|
||||||
if (LOGGING_ENABLED) {
|
if (LOGGING_ENABLED) {
|
||||||
console.log(token.workspace.name, 'reloading workspace', JSON.stringify(token))
|
console.log(workspaceName, 'reloading workspace', JSON.stringify(token))
|
||||||
}
|
}
|
||||||
// If upgrade client is used.
|
// If upgrade client is used.
|
||||||
// Drop all existing clients
|
// Drop all existing clients
|
||||||
await this.closeAll(wsString, workspace, 0, 'upgrade')
|
await this.closeAll(wsString, workspace, 0, 'upgrade')
|
||||||
// Wipe workspace and update values.
|
// Wipe workspace and update values.
|
||||||
|
workspace.workspaceName = workspaceName
|
||||||
if (!workspace.upgrade) {
|
if (!workspace.upgrade) {
|
||||||
// This is previous workspace, intended to be closed.
|
// This is previous workspace, intended to be closed.
|
||||||
workspace.id = generateId()
|
workspace.id = generateId()
|
||||||
@ -238,9 +296,14 @@ class TSessionManager implements SessionManager {
|
|||||||
workspace.upgrade = token.extra?.model === 'upgrade'
|
workspace.upgrade = token.extra?.model === 'upgrade'
|
||||||
}
|
}
|
||||||
// Re-create pipeline.
|
// Re-create pipeline.
|
||||||
workspace.pipeline = pipelineFactory(ctx, token.workspace, true, (tx, targets) => {
|
workspace.pipeline = pipelineFactory(
|
||||||
|
ctx,
|
||||||
|
{ ...token.workspace, workspaceUrl, workspaceName },
|
||||||
|
true,
|
||||||
|
(tx, targets) => {
|
||||||
this.broadcastAll(workspace, tx, targets)
|
this.broadcastAll(workspace, tx, targets)
|
||||||
})
|
}
|
||||||
|
)
|
||||||
return await workspace.pipeline
|
return await workspace.pipeline
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -271,20 +334,32 @@ class TSessionManager implements SessionManager {
|
|||||||
send()
|
send()
|
||||||
}
|
}
|
||||||
|
|
||||||
private createWorkspace (ctx: MeasureContext, pipelineFactory: PipelineFactory, token: Token): Workspace {
|
private createWorkspace (
|
||||||
|
ctx: MeasureContext,
|
||||||
|
pipelineFactory: PipelineFactory,
|
||||||
|
token: Token,
|
||||||
|
workspaceUrl: string,
|
||||||
|
workspaceName: string
|
||||||
|
): Workspace {
|
||||||
const upgrade = token.extra?.model === 'upgrade'
|
const upgrade = token.extra?.model === 'upgrade'
|
||||||
const context = ctx.newChild('🧲 ' + token.workspace.name, {})
|
const context = ctx.newChild('🧲 session', {})
|
||||||
const workspace: Workspace = {
|
const workspace: Workspace = {
|
||||||
context,
|
context,
|
||||||
id: generateId(),
|
id: generateId(),
|
||||||
pipeline: pipelineFactory(context, token.workspace, upgrade, (tx, targets) => {
|
pipeline: pipelineFactory(
|
||||||
|
context,
|
||||||
|
{ ...token.workspace, workspaceUrl, workspaceName },
|
||||||
|
upgrade,
|
||||||
|
(tx, targets) => {
|
||||||
this.broadcastAll(workspace, tx, targets)
|
this.broadcastAll(workspace, tx, targets)
|
||||||
}),
|
|
||||||
sessions: new Map(),
|
|
||||||
upgrade
|
|
||||||
}
|
}
|
||||||
if (LOGGING_ENABLED) console.time(token.workspace.name)
|
),
|
||||||
if (LOGGING_ENABLED) console.timeLog(token.workspace.name, 'Creating Workspace:', workspace.id)
|
sessions: new Map(),
|
||||||
|
upgrade,
|
||||||
|
workspaceName
|
||||||
|
}
|
||||||
|
if (LOGGING_ENABLED) console.time(workspaceName)
|
||||||
|
if (LOGGING_ENABLED) console.timeLog(workspaceName, 'Creating Workspace:', workspace.id)
|
||||||
this.workspaces.set(toWorkspaceString(token.workspace), workspace)
|
this.workspaces.set(toWorkspaceString(token.workspace), workspace)
|
||||||
return workspace
|
return workspace
|
||||||
}
|
}
|
||||||
@ -498,7 +573,9 @@ class TSessionManager implements SessionManager {
|
|||||||
msg: any,
|
msg: any,
|
||||||
workspace: string
|
workspace: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const userCtx = requestCtx.newChild('📞 client', {}) as SessionContext
|
const userCtx = requestCtx.newChild('📞 client', {
|
||||||
|
workspace: '🧲 ' + workspace
|
||||||
|
}) as SessionContext
|
||||||
userCtx.sessionId = service.sessionInstanceId ?? ''
|
userCtx.sessionId = service.sessionInstanceId ?? ''
|
||||||
|
|
||||||
// Calculate total number of clients
|
// Calculate total number of clients
|
||||||
@ -668,6 +745,7 @@ export function start (
|
|||||||
productId: string
|
productId: string
|
||||||
serverFactory: ServerFactory
|
serverFactory: ServerFactory
|
||||||
enableCompression?: boolean
|
enableCompression?: boolean
|
||||||
|
accountsUrl: string
|
||||||
} & Partial<Timeouts>
|
} & Partial<Timeouts>
|
||||||
): () => Promise<void> {
|
): () => Promise<void> {
|
||||||
const sessions = new TSessionManager(ctx, opt.sessionFactory, {
|
const sessions = new TSessionManager(ctx, opt.sessionFactory, {
|
||||||
@ -682,6 +760,7 @@ export function start (
|
|||||||
opt.pipelineFactory,
|
opt.pipelineFactory,
|
||||||
opt.port,
|
opt.port,
|
||||||
opt.productId,
|
opt.productId,
|
||||||
opt.enableCompression ?? true
|
opt.enableCompression ?? true,
|
||||||
|
opt.accountsUrl
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -13,20 +13,20 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
import { type MeasureContext, generateId } from '@hcengineering/core'
|
import { generateId, type MeasureContext } from '@hcengineering/core'
|
||||||
import { UNAUTHORIZED } from '@hcengineering/platform'
|
import { UNAUTHORIZED } from '@hcengineering/platform'
|
||||||
import { type Response, serialize } from '@hcengineering/rpc'
|
import { serialize, type Response } from '@hcengineering/rpc'
|
||||||
import { type Token, decodeToken } from '@hcengineering/server-token'
|
import { decodeToken, type Token } from '@hcengineering/server-token'
|
||||||
import compression from 'compression'
|
import compression from 'compression'
|
||||||
import cors from 'cors'
|
import cors from 'cors'
|
||||||
import express from 'express'
|
import express from 'express'
|
||||||
import http, { type IncomingMessage } from 'http'
|
import http, { type IncomingMessage } from 'http'
|
||||||
import { type RawData, type WebSocket, WebSocketServer } from 'ws'
|
import { WebSocketServer, type RawData, type WebSocket } from 'ws'
|
||||||
import { getStatistics } from './stats'
|
import { getStatistics } from './stats'
|
||||||
import {
|
import {
|
||||||
|
LOGGING_ENABLED,
|
||||||
type ConnectionSocket,
|
type ConnectionSocket,
|
||||||
type HandleRequestFunction,
|
type HandleRequestFunction,
|
||||||
LOGGING_ENABLED,
|
|
||||||
type PipelineFactory,
|
type PipelineFactory,
|
||||||
type SessionManager
|
type SessionManager
|
||||||
} from './types'
|
} from './types'
|
||||||
@ -44,7 +44,8 @@ export function startHttpServer (
|
|||||||
pipelineFactory: PipelineFactory,
|
pipelineFactory: PipelineFactory,
|
||||||
port: number,
|
port: number,
|
||||||
productId: string,
|
productId: string,
|
||||||
enableCompression: boolean
|
enableCompression: boolean,
|
||||||
|
accountsUrl: string
|
||||||
): () => Promise<void> {
|
): () => Promise<void> {
|
||||||
if (LOGGING_ENABLED) console.log(`starting server on port ${port} ...`)
|
if (LOGGING_ENABLED) console.log(`starting server on port ${port} ...`)
|
||||||
|
|
||||||
@ -151,7 +152,13 @@ export function startHttpServer (
|
|||||||
skipUTF8Validation: true
|
skipUTF8Validation: true
|
||||||
})
|
})
|
||||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
wss.on('connection', async (ws: WebSocket, request: IncomingMessage, token: Token, sessionId?: string) => {
|
const handleConnection = async (
|
||||||
|
ws: WebSocket,
|
||||||
|
request: IncomingMessage,
|
||||||
|
token: Token,
|
||||||
|
rawToken: string,
|
||||||
|
sessionId?: string
|
||||||
|
): Promise<void> => {
|
||||||
let buffer: Buffer[] | undefined = []
|
let buffer: Buffer[] | undefined = []
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
@ -190,8 +197,20 @@ export function startHttpServer (
|
|||||||
ws.on('message', (msg: Buffer) => {
|
ws.on('message', (msg: Buffer) => {
|
||||||
buffer?.push(msg)
|
buffer?.push(msg)
|
||||||
})
|
})
|
||||||
const session = await sessions.addSession(ctx, cs, token, pipelineFactory, productId, sessionId)
|
const session = await sessions.addSession(
|
||||||
if ('upgrade' in session) {
|
ctx,
|
||||||
|
cs,
|
||||||
|
token,
|
||||||
|
rawToken,
|
||||||
|
pipelineFactory,
|
||||||
|
productId,
|
||||||
|
sessionId,
|
||||||
|
accountsUrl
|
||||||
|
)
|
||||||
|
if ('upgrade' in session || 'error' in session) {
|
||||||
|
if ('error' in session) {
|
||||||
|
console.error(session.error)
|
||||||
|
}
|
||||||
cs.close()
|
cs.close()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -204,7 +223,7 @@ export function startHttpServer (
|
|||||||
buff = Buffer.concat(msg).toString()
|
buff = Buffer.concat(msg).toString()
|
||||||
}
|
}
|
||||||
if (buff !== undefined) {
|
if (buff !== undefined) {
|
||||||
void handleRequest(session.context, session.session, cs, buff, token.workspace.name)
|
void handleRequest(session.context, session.session, cs, buff, session.workspaceName)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
@ -218,9 +237,10 @@ export function startHttpServer (
|
|||||||
const b = buffer
|
const b = buffer
|
||||||
buffer = undefined
|
buffer = undefined
|
||||||
for (const msg of b) {
|
for (const msg of b) {
|
||||||
await handleRequest(session.context, session.session, cs, msg, token.workspace.name)
|
await handleRequest(session.context, session.session, cs, msg, session.workspaceName)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
wss.on('connection', handleConnection as any)
|
||||||
|
|
||||||
httpServer.on('upgrade', (request: IncomingMessage, socket: any, head: Buffer) => {
|
httpServer.on('upgrade', (request: IncomingMessage, socket: any, head: Buffer) => {
|
||||||
const url = new URL('http://localhost' + (request.url ?? ''))
|
const url = new URL('http://localhost' + (request.url ?? ''))
|
||||||
@ -234,7 +254,7 @@ export function startHttpServer (
|
|||||||
throw new Error('Invalid workspace product')
|
throw new Error('Invalid workspace product')
|
||||||
}
|
}
|
||||||
|
|
||||||
wss.handleUpgrade(request, socket, head, (ws) => wss.emit('connection', ws, request, payload, sessionId))
|
wss.handleUpgrade(request, socket, head, (ws) => wss.emit('connection', ws, request, payload, token, sessionId))
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (LOGGING_ENABLED) console.error('invalid token', err)
|
if (LOGGING_ENABLED) console.error('invalid token', err)
|
||||||
wss.handleUpgrade(request, socket, head, (ws) => {
|
wss.handleUpgrade(request, socket, head, (ws) => {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
type WorkspaceIdWithUrl,
|
||||||
type Class,
|
type Class,
|
||||||
type Doc,
|
type Doc,
|
||||||
type DocumentQuery,
|
type DocumentQuery,
|
||||||
@ -78,7 +79,7 @@ export type BroadcastCall = (
|
|||||||
*/
|
*/
|
||||||
export type PipelineFactory = (
|
export type PipelineFactory = (
|
||||||
ctx: MeasureContext,
|
ctx: MeasureContext,
|
||||||
ws: WorkspaceId,
|
ws: WorkspaceIdWithUrl,
|
||||||
upgrade: boolean,
|
upgrade: boolean,
|
||||||
broadcast: BroadcastFunc
|
broadcast: BroadcastFunc
|
||||||
) => Promise<Pipeline>
|
) => Promise<Pipeline>
|
||||||
@ -115,6 +116,8 @@ export interface Workspace {
|
|||||||
sessions: Map<string, { session: Session, socket: ConnectionSocket }>
|
sessions: Map<string, { session: Session, socket: ConnectionSocket }>
|
||||||
upgrade: boolean
|
upgrade: boolean
|
||||||
closing?: Promise<void>
|
closing?: Promise<void>
|
||||||
|
|
||||||
|
workspaceName: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -130,10 +133,14 @@ export interface SessionManager {
|
|||||||
ctx: MeasureContext,
|
ctx: MeasureContext,
|
||||||
ws: ConnectionSocket,
|
ws: ConnectionSocket,
|
||||||
token: Token,
|
token: Token,
|
||||||
|
rawToken: string,
|
||||||
pipelineFactory: PipelineFactory,
|
pipelineFactory: PipelineFactory,
|
||||||
productId: string,
|
productId: string,
|
||||||
sessionId?: string
|
sessionId: string | undefined,
|
||||||
) => Promise<{ session: Session, context: MeasureContext } | { upgrade: true }>
|
accountsUrl: string
|
||||||
|
) => Promise<
|
||||||
|
{ session: Session, context: MeasureContext, workspaceName: string } | { upgrade: true } | { error: any }
|
||||||
|
>
|
||||||
|
|
||||||
broadcastAll: (workspace: Workspace, tx: Tx[], targets?: string[]) => void
|
broadcastAll: (workspace: Workspace, tx: Tx[], targets?: string[]) => void
|
||||||
|
|
||||||
@ -170,5 +177,6 @@ export type ServerFactory = (
|
|||||||
pipelineFactory: PipelineFactory,
|
pipelineFactory: PipelineFactory,
|
||||||
port: number,
|
port: number,
|
||||||
productId: string,
|
productId: string,
|
||||||
enableCompression: boolean
|
enableCompression: boolean,
|
||||||
|
accountsUrl: string
|
||||||
) => () => Promise<void>
|
) => () => Promise<void>
|
||||||
|
@ -37,7 +37,6 @@ services:
|
|||||||
links:
|
links:
|
||||||
- mongodb
|
- mongodb
|
||||||
- minio
|
- minio
|
||||||
- transactor
|
|
||||||
ports:
|
ports:
|
||||||
- 3003:3003
|
- 3003:3003
|
||||||
environment:
|
environment:
|
||||||
@ -85,6 +84,7 @@ services:
|
|||||||
- elastic
|
- elastic
|
||||||
- minio
|
- minio
|
||||||
- rekoni
|
- rekoni
|
||||||
|
- account
|
||||||
ports:
|
ports:
|
||||||
- 3334:3334
|
- 3334:3334
|
||||||
environment:
|
environment:
|
||||||
@ -99,6 +99,7 @@ services:
|
|||||||
- MINIO_SECRET_KEY=minioadmin
|
- MINIO_SECRET_KEY=minioadmin
|
||||||
- REKONI_URL=http://rekoni:4005
|
- REKONI_URL=http://rekoni:4005
|
||||||
- FRONT_URL=http://localhost:8083
|
- FRONT_URL=http://localhost:8083
|
||||||
|
- ACCOUNTS_URL=http://account:3003
|
||||||
collaborator:
|
collaborator:
|
||||||
image: hardcoreeng/collaborator
|
image: hardcoreeng/collaborator
|
||||||
links:
|
links:
|
||||||
|
@ -7,10 +7,12 @@ docker compose -p sanity up -d --force-recreate --renew-anon-volumes
|
|||||||
./wait-elastic.sh 9201
|
./wait-elastic.sh 9201
|
||||||
|
|
||||||
# Create workspace record in accounts
|
# Create workspace record in accounts
|
||||||
./tool.sh create-workspace sanity-ws -o SanityTest
|
./tool.sh create-workspace sanity-ws -w SanityTest
|
||||||
# Create user record in accounts
|
# Create user record in accounts
|
||||||
./tool.sh create-account user1 -f John -l Appleseed -p 1234
|
./tool.sh create-account user1 -f John -l Appleseed -p 1234
|
||||||
./tool.sh create-account user2 -f Kainin -l Dirak -p 1234
|
./tool.sh create-account user2 -f Kainin -l Dirak -p 1234
|
||||||
|
./tool.sh assign-workspace user1 sanity-ws
|
||||||
|
./tool.sh assign-workspace user2 sanity-ws
|
||||||
# Make user the workspace maintainer
|
# Make user the workspace maintainer
|
||||||
./tool.sh set-user-role user1 sanity-ws 1
|
./tool.sh set-user-role user1 sanity-ws 1
|
||||||
./tool.sh confirm-email user1
|
./tool.sh confirm-email user1
|
||||||
|
@ -14,6 +14,7 @@ node ../dev/tool/bundle.js upgrade-workspace sanity-ws
|
|||||||
|
|
||||||
# Re-assign user to workspace.
|
# Re-assign user to workspace.
|
||||||
node ../dev/tool/bundle.js assign-workspace user1 sanity-ws
|
node ../dev/tool/bundle.js assign-workspace user1 sanity-ws
|
||||||
|
node ../dev/tool/bundle.js assign-workspace user2 sanity-ws
|
||||||
|
|
||||||
node ../dev/tool/bundle.js configure sanity-ws --enable=*
|
node ../dev/tool/bundle.js configure sanity-ws --enable=*
|
||||||
node ../dev/tool/bundle.js configure sanity-ws --list
|
node ../dev/tool/bundle.js configure sanity-ws --list
|
@ -17,6 +17,6 @@ test.describe('login test', () => {
|
|||||||
await loginPage.login(PlatformUser, '1234')
|
await loginPage.login(PlatformUser, '1234')
|
||||||
|
|
||||||
const selectWorkspacePage = new SelectWorkspacePage(page)
|
const selectWorkspacePage = new SelectWorkspacePage(page)
|
||||||
await selectWorkspacePage.selectWorkspace('sanity-ws')
|
await selectWorkspacePage.selectWorkspace('SanityTest')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user