mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-25 01:04:55 +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
|
||||
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 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.
|
||||
|
@ -155,7 +155,7 @@ export async function connect (handler: (tx: Tx) => void): Promise<ClientConnect
|
||||
}
|
||||
},
|
||||
defaultContentAdapter: 'default',
|
||||
workspace: getWorkspaceId('')
|
||||
workspace: { ...getWorkspaceId(''), workspaceUrl: '', workspaceName: '' }
|
||||
}
|
||||
const ctx = new MeasureMetricsContext('client', {})
|
||||
const serverStorage = await createServerStorage(ctx, conf, {
|
||||
|
@ -46,7 +46,6 @@ services:
|
||||
links:
|
||||
- mongodb
|
||||
- minio
|
||||
- transactor
|
||||
ports:
|
||||
- 3000:3000
|
||||
environment:
|
||||
@ -136,6 +135,7 @@ services:
|
||||
- elastic
|
||||
- minio
|
||||
- rekoni
|
||||
- account
|
||||
# - apm-server
|
||||
ports:
|
||||
- 3333:3333
|
||||
@ -154,6 +154,7 @@ services:
|
||||
- FRONT_URL=http://localhost:8087
|
||||
# - APM_SERVER_URL=http://apm-server:8200
|
||||
- SERVER_PROVIDER=ws
|
||||
- ACCOUNTS_URL=http://account:3000
|
||||
restart: unless-stopped
|
||||
rekoni:
|
||||
image: hardcoreeng/rekoni-service
|
||||
|
@ -14,7 +14,7 @@
|
||||
// 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 {
|
||||
ContentTextAdapter,
|
||||
@ -44,7 +44,7 @@ async function createNullContentTextAdapter (): Promise<ContentTextAdapter> {
|
||||
export async function start (port: number, host?: string): Promise<void> {
|
||||
const ctx = new MeasureMetricsContext('server', {})
|
||||
startJsonRpc(ctx, {
|
||||
pipelineFactory: (ctx) => {
|
||||
pipelineFactory: (ctx, workspaceId) => {
|
||||
const conf: DbConfiguration = {
|
||||
domains: {
|
||||
[DOMAIN_TX]: 'InMemoryTx'
|
||||
@ -74,13 +74,14 @@ export async function start (port: number, host?: string): Promise<void> {
|
||||
}
|
||||
},
|
||||
defaultContentAdapter: 'default',
|
||||
workspace: getWorkspaceId('')
|
||||
workspace: workspaceId
|
||||
}
|
||||
return createPipeline(ctx, conf, [], false, () => {})
|
||||
},
|
||||
sessionFactory: (token, pipeline, broadcast) => new ClientSession(broadcast, token, pipeline),
|
||||
port,
|
||||
productId: '',
|
||||
serverFactory: startHttpServer
|
||||
serverFactory: startHttpServer,
|
||||
accountsUrl: ''
|
||||
})
|
||||
}
|
||||
|
@ -207,14 +207,19 @@ export async function benchmark (
|
||||
operations = 0
|
||||
requestTime = 0
|
||||
transfer = 0
|
||||
for (const w of workspaceId) {
|
||||
const r = extract(json.metrics as Metrics, w.name, 'client', 'handleRequest', 'process', 'find-all')
|
||||
operations += r?.operations ?? 0
|
||||
requestTime += (r?.value ?? 0) / (((r?.operations as number) ?? 0) + 1)
|
||||
const r = extract(
|
||||
json.metrics as Metrics,
|
||||
'🧲 session',
|
||||
'client',
|
||||
'handleRequest',
|
||||
'process',
|
||||
'find-all'
|
||||
)
|
||||
operations += r?.operations ?? 0
|
||||
requestTime += (r?.value ?? 0) / (((r?.operations as number) ?? 0) + 1)
|
||||
|
||||
const tr = extract(json.metrics as Metrics, w.name, 'client', 'handleRequest', '#send-data')
|
||||
transfer += tr?.value ?? 0
|
||||
}
|
||||
const tr = extract(json.metrics as Metrics, '🧲 session', 'client', 'handleRequest', '#send-data')
|
||||
transfer += tr?.value ?? 0
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
|
@ -17,20 +17,20 @@
|
||||
import {
|
||||
ACCOUNT_DB,
|
||||
assignWorkspace,
|
||||
ClientWorkspaceInfo,
|
||||
confirmEmail,
|
||||
createAcc,
|
||||
createWorkspace,
|
||||
dropAccount,
|
||||
dropWorkspace,
|
||||
getAccount,
|
||||
getWorkspace,
|
||||
getWorkspaceById,
|
||||
listAccounts,
|
||||
listWorkspaces,
|
||||
replacePassword,
|
||||
setAccountAdmin,
|
||||
setRole,
|
||||
upgradeWorkspace,
|
||||
WorkspaceInfoOnly
|
||||
upgradeWorkspace
|
||||
} from '@hcengineering/account'
|
||||
import { setMetadata } from '@hcengineering/platform'
|
||||
import {
|
||||
@ -43,7 +43,7 @@ import {
|
||||
import serverToken, { decodeToken, generateToken } from '@hcengineering/server-token'
|
||||
import toolPlugin, { FileModelLogger } from '@hcengineering/server-tool'
|
||||
|
||||
import { program, Command } from 'commander'
|
||||
import { Command, program } from 'commander'
|
||||
import { Db, MongoClient } from 'mongodb'
|
||||
import { clearTelegramHistory } from './telegram'
|
||||
import { diffWorkspace } from './workspace'
|
||||
@ -155,7 +155,16 @@ export function devTool (
|
||||
const { mongodbUri } = prepareTools()
|
||||
await withDatabase(mongodbUri, async (db, client) => {
|
||||
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
|
||||
.command('create-workspace <name>')
|
||||
.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) => {
|
||||
const { mongodbUri, txes, version, migrateOperations } = prepareTools()
|
||||
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) => {
|
||||
const { mongodbUri, version, txes, migrateOperations } = prepareTools()
|
||||
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 withError: string[] = []
|
||||
|
||||
async function _upgradeWorkspace (ws: WorkspaceInfoOnly): Promise<void> {
|
||||
async function _upgradeWorkspace (ws: ClientWorkspaceInfo): Promise<void> {
|
||||
const t = Date.now()
|
||||
const logger = cmd.console
|
||||
? consoleModelLogger
|
||||
@ -298,7 +312,7 @@ export function devTool (
|
||||
.action(async (workspace, cmd) => {
|
||||
const { mongodbUri } = prepareTools()
|
||||
await withDatabase(mongodbUri, async (db) => {
|
||||
const ws = await getWorkspace(db, productId, workspace)
|
||||
const ws = await getWorkspaceById(db, productId, workspace)
|
||||
if (ws === null) {
|
||||
console.log('no workspace exists')
|
||||
return
|
||||
|
@ -229,7 +229,7 @@ export async function createClient (
|
||||
let lastTx: number
|
||||
|
||||
function txHandler (tx: Tx): void {
|
||||
if (tx === null) {
|
||||
if (tx == null) {
|
||||
return
|
||||
}
|
||||
if (client === null) {
|
||||
|
@ -45,8 +45,8 @@ function count (): string {
|
||||
* @public
|
||||
* @returns
|
||||
*/
|
||||
export function generateId<T extends Doc> (): Ref<T> {
|
||||
return (timestamp() + random + count()) as Ref<T>
|
||||
export function generateId<T extends Doc> (join: string = ''): Ref<T> {
|
||||
return (timestamp() + join + random + join + count()) as Ref<T>
|
||||
}
|
||||
|
||||
let currentAccount: Account
|
||||
@ -89,6 +89,14 @@ export interface WorkspaceId {
|
||||
productId: string
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface WorkspaceIdWithUrl extends WorkspaceId {
|
||||
workspaceUrl: string
|
||||
workspaceName: string
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*
|
||||
@ -507,7 +515,7 @@ export function cutObjectArray (obj: any): any {
|
||||
for (const key of Object.keys(obj)) {
|
||||
if (Array.isArray(obj[key])) {
|
||||
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] })
|
||||
continue
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ import core, {
|
||||
AccountClient,
|
||||
ClientConnectEvent,
|
||||
TxHandler,
|
||||
TxPersistenceStore,
|
||||
TxWorkspaceEvent,
|
||||
WorkspaceEvent,
|
||||
createClient
|
||||
@ -68,34 +69,7 @@ export default async () => {
|
||||
return connect(url.href, upgradeHandler, onUpgrade, onUnauthorized, onConnect)
|
||||
},
|
||||
filterModel ? [...getPlugins(), ...(getMetadata(clientPlugin.metadata.ExtraPlugins) ?? [])] : undefined,
|
||||
{
|
||||
load: async () => {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
const storedValue = localStorage.getItem('platform.model') ?? null
|
||||
try {
|
||||
const model = storedValue != null ? JSON.parse(storedValue) : undefined
|
||||
if (token !== model?.token) {
|
||||
return {
|
||||
full: false,
|
||||
transactions: [],
|
||||
hash: []
|
||||
}
|
||||
}
|
||||
return model.model
|
||||
} catch {}
|
||||
}
|
||||
return {
|
||||
full: true,
|
||||
transactions: [],
|
||||
hash: []
|
||||
}
|
||||
},
|
||||
store: async (model) => {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem('platform.model', JSON.stringify({ token, model }))
|
||||
}
|
||||
}
|
||||
}
|
||||
createModelPersistence(token)
|
||||
)
|
||||
// Check if we had dev hook for client.
|
||||
client = hookClient(client)
|
||||
@ -104,6 +78,37 @@ export default async () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
function createModelPersistence (token: string): TxPersistenceStore | undefined {
|
||||
return {
|
||||
load: async () => {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
const storedValue = localStorage.getItem('platform.model') ?? null
|
||||
try {
|
||||
const model = storedValue != null ? JSON.parse(storedValue) : undefined
|
||||
if (token !== model?.token) {
|
||||
return {
|
||||
full: false,
|
||||
transactions: [],
|
||||
hash: []
|
||||
}
|
||||
}
|
||||
return model.model
|
||||
} catch {}
|
||||
}
|
||||
return {
|
||||
full: true,
|
||||
transactions: [],
|
||||
hash: []
|
||||
}
|
||||
},
|
||||
store: async (model) => {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem('platform.model', JSON.stringify({ token, model }))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function hookClient (client: Promise<AccountClient>): Promise<AccountClient> {
|
||||
const hook = getMetadata(clientPlugin.metadata.ClientHook)
|
||||
if (hook !== undefined) {
|
||||
|
@ -15,7 +15,7 @@
|
||||
"Join": "Join",
|
||||
"Email": "Email",
|
||||
"Password": "Password",
|
||||
"Workspace": "Workspace",
|
||||
"Workspace": "Workspace name",
|
||||
"DoNotHaveAnAccount": "Do not have an account?",
|
||||
"PasswordRepeat": "Repeat password",
|
||||
"HaveAccount": "Already have an account?",
|
||||
@ -27,12 +27,6 @@
|
||||
"WantAnotherWorkspace": "Want to create another workspace?",
|
||||
"ChangeAccount": "Change account",
|
||||
"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?",
|
||||
"KnowPassword": "Know your password?",
|
||||
"Recover": "Recover",
|
||||
|
@ -27,12 +27,6 @@
|
||||
"WantAnotherWorkspace": "Хотите создать другое рабочее пространство?",
|
||||
"ChangeAccount": "Сменить пользователя",
|
||||
"NotSeeingWorkspace": "Не видите ваше рабочее пространство?",
|
||||
"WorkspaceNameRule": "Название рабочего пространства не может содержать специальные символы кроме -",
|
||||
"WorkspaceNameRuleCapital": "Название рабочего пространства должно содержать только строчные буквы",
|
||||
"WorkspaceNameRuleHyphen": "Название рабочего пространства не может начинаться с символа 'тире' (-)",
|
||||
"WorkspaceNameRuleHyphenEnd": "Название рабочего пространства не может заканчиваться символом 'тире' (-)",
|
||||
"WorkspaceNameRuleLengthLow": "Название рабочего пространства должно быть не короче 3 символов",
|
||||
"WorkspaceNameRuleLengthHigh": "Название рабочего пространства должно быть не длиннее 63 символов",
|
||||
"ForgotPassword": "Забыли пароль?",
|
||||
"KnowPassword": "Знаете пароль?",
|
||||
"Recover": "Восстановить",
|
||||
|
@ -31,7 +31,7 @@
|
||||
navigate(loc)
|
||||
}
|
||||
|
||||
function goToLogin () {
|
||||
function goToLogin (): void {
|
||||
const loc = getCurrentLocation()
|
||||
loc.query = undefined
|
||||
loc.path[1] = 'login'
|
||||
@ -39,7 +39,7 @@
|
||||
navigate(loc)
|
||||
}
|
||||
|
||||
async function check () {
|
||||
async function check (): Promise<void> {
|
||||
const location = getCurrentLocation()
|
||||
if (location.query?.id === undefined || location.query?.id === null) return
|
||||
status = new Status(Severity.INFO, login.status.ConnectingToServer, {})
|
||||
@ -50,6 +50,7 @@
|
||||
|
||||
if (result !== undefined) {
|
||||
setMetadata(presentation.metadata.Token, result.token)
|
||||
setMetadataLocalStorage(login.metadata.LastToken, result.token)
|
||||
setMetadataLocalStorage(login.metadata.LoginEndpoint, result.endpoint)
|
||||
setMetadataLocalStorage(login.metadata.LoginEmail, result.email)
|
||||
goToWorkspaces()
|
||||
|
@ -28,38 +28,7 @@
|
||||
{
|
||||
name: 'workspace',
|
||||
i18n: login.string.Workspace,
|
||||
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
|
||||
}
|
||||
]
|
||||
rules: []
|
||||
}
|
||||
]
|
||||
|
||||
@ -89,12 +58,13 @@
|
||||
|
||||
if (result !== undefined) {
|
||||
setMetadata(presentation.metadata.Token, result.token)
|
||||
setMetadataLocalStorage(login.metadata.LastToken, result.token)
|
||||
const tokens: Record<string, string> = fetchMetadataLocalStorage(login.metadata.LoginTokens) ?? {}
|
||||
tokens[object.workspace] = result.token
|
||||
setMetadataLocalStorage(login.metadata.LoginTokens, tokens)
|
||||
setMetadataLocalStorage(login.metadata.LoginEndpoint, result.endpoint)
|
||||
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) {
|
||||
const v = object[field.name]
|
||||
const f = field
|
||||
if (!f.optional && (!v || v === '')) {
|
||||
if (!f.optional && (!v || v.trim() === '')) {
|
||||
status = new Status(Severity.INFO, login.status.RequiredField, {
|
||||
field: await translate(field.i18n, {}, language)
|
||||
})
|
||||
|
@ -75,6 +75,7 @@
|
||||
|
||||
if (result !== undefined) {
|
||||
setMetadata(presentation.metadata.Token, result.token)
|
||||
setMetadataLocalStorage(login.metadata.LastToken, result.token)
|
||||
const tokens: Record<string, string> = fetchMetadataLocalStorage(login.metadata.LoginTokens) ?? {}
|
||||
tokens[result.workspace] = result.token
|
||||
setMetadataLocalStorage(login.metadata.LoginTokens, tokens)
|
||||
@ -128,10 +129,10 @@
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
check()
|
||||
void check()
|
||||
})
|
||||
|
||||
async function check () {
|
||||
async function check (): Promise<void> {
|
||||
if (location.query?.inviteId === undefined || location.query?.inviteId === null) return
|
||||
status = new Status(Severity.INFO, login.status.ConnectingToServer, {})
|
||||
const [, result] = await checkJoined(location.query.inviteId)
|
||||
@ -139,6 +140,7 @@
|
||||
if (result !== undefined) {
|
||||
const tokens: Record<string, string> = fetchMetadataLocalStorage(login.metadata.LoginTokens) ?? {}
|
||||
setMetadata(presentation.metadata.Token, result.token)
|
||||
setMetadataLocalStorage(login.metadata.LastToken, result.token)
|
||||
tokens[result.workspace] = result.token
|
||||
setMetadataLocalStorage(login.metadata.LoginTokens, tokens)
|
||||
setMetadataLocalStorage(login.metadata.LoginEndpoint, result.endpoint)
|
||||
|
@ -63,6 +63,7 @@
|
||||
onDestroy(
|
||||
location.subscribe((loc) => {
|
||||
void (async (loc) => {
|
||||
token = getMetadata(presentation.metadata.Token)
|
||||
page = loc.path[1] ?? (token ? 'selectWorkspace' : 'login')
|
||||
if (!pages.includes(page)) {
|
||||
page = 'login'
|
||||
|
@ -14,13 +14,21 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { OK, setMetadata, Severity, Status } from '@hcengineering/platform'
|
||||
import { getCurrentLocation, navigate, Location, setMetadataLocalStorage } from '@hcengineering/ui'
|
||||
import { getMetadata, OK, setMetadata, Severity, Status } from '@hcengineering/platform'
|
||||
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 { LoginInfo } from '@hcengineering/login'
|
||||
import login from '../plugin'
|
||||
|
||||
export let navigateUrl: string | undefined = undefined
|
||||
@ -40,48 +48,62 @@
|
||||
password: ''
|
||||
}
|
||||
|
||||
async function doLoginNavigate (
|
||||
result: LoginInfo | undefined,
|
||||
updateStatus: (status: Status<any>) => void,
|
||||
token?: string
|
||||
): Promise<boolean> {
|
||||
if (result !== undefined) {
|
||||
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.LoginEmail, result.email)
|
||||
|
||||
if (navigateUrl !== undefined) {
|
||||
try {
|
||||
const loc = JSON.parse(decodeURIComponent(navigateUrl)) as Location
|
||||
const workspace = loc.path[1]
|
||||
if (workspace !== undefined) {
|
||||
const workspaces = await getWorkspaces()
|
||||
if (workspaces.find((p) => p.workspace === workspace) !== undefined) {
|
||||
updateStatus(new Status(Severity.INFO, login.status.ConnectingToServer, {}))
|
||||
|
||||
const [loginStatus, result] = await selectWorkspace(workspace)
|
||||
updateStatus(loginStatus)
|
||||
navigateToWorkspace(workspace, result, navigateUrl)
|
||||
return true
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
// Json parse error could be ignored
|
||||
}
|
||||
}
|
||||
const loc = getCurrentLocation()
|
||||
loc.path[1] = result.confirmed ? 'selectWorkspace' : 'confirmationSend'
|
||||
loc.path.length = 2
|
||||
if (navigateUrl !== undefined) {
|
||||
loc.query = { ...loc.query, navigateUrl }
|
||||
}
|
||||
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
|
||||
|
||||
if (result !== undefined) {
|
||||
setMetadata(presentation.metadata.Token, result.token)
|
||||
setMetadataLocalStorage(login.metadata.LoginEndpoint, result.endpoint)
|
||||
setMetadataLocalStorage(login.metadata.LoginEmail, result.email)
|
||||
|
||||
if (navigateUrl !== undefined) {
|
||||
try {
|
||||
const loc = JSON.parse(decodeURIComponent(navigateUrl)) as Location
|
||||
const workspace = loc.path[1]
|
||||
if (workspace !== undefined) {
|
||||
const workspaces = await getWorkspaces()
|
||||
if (workspaces.find((p) => p.workspace === workspace) !== undefined) {
|
||||
status = new Status(Severity.INFO, login.status.ConnectingToServer, {})
|
||||
|
||||
const [loginStatus, result] = await selectWorkspace(workspace)
|
||||
status = loginStatus
|
||||
navigateToWorkspace(workspace, result, navigateUrl)
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
// Json parse error could be ignored
|
||||
}
|
||||
}
|
||||
const loc = getCurrentLocation()
|
||||
loc.path[1] = result.confirmed ? 'selectWorkspace' : 'confirmationSend'
|
||||
loc.path.length = 2
|
||||
if (navigateUrl !== undefined) {
|
||||
loc.query = { ...loc.query, navigateUrl }
|
||||
}
|
||||
navigate(loc)
|
||||
}
|
||||
await doLoginNavigate(result, (st) => {
|
||||
status = st
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -95,6 +117,30 @@
|
||||
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>
|
||||
|
||||
<Form
|
||||
|
@ -111,8 +111,11 @@
|
||||
}
|
||||
</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="fs-title">
|
||||
{account?.email}
|
||||
</div>
|
||||
<div class="title"><Label label={login.string.SelectWorkspace} /></div>
|
||||
<div class="status">
|
||||
<StatusControl {status} />
|
||||
@ -126,7 +129,7 @@
|
||||
class="workspace flex-center fs-title cursor-pointer focused-button bordered form-row"
|
||||
on:click={() => select(workspace.workspace)}
|
||||
>
|
||||
{workspace.workspace}
|
||||
{workspace.workspaceName ?? workspace.workspace}
|
||||
</div>
|
||||
{/each}
|
||||
{#if workspaces.length === 0 && account?.confirmed === true}
|
||||
|
@ -16,7 +16,7 @@
|
||||
<script lang="ts">
|
||||
import { OK, Severity, Status, setMetadata } from '@hcengineering/platform'
|
||||
import presentation from '@hcengineering/presentation'
|
||||
import { getCurrentLocation, navigate } from '@hcengineering/ui'
|
||||
import { getCurrentLocation, navigate, setMetadataLocalStorage } from '@hcengineering/ui'
|
||||
import login from '../plugin'
|
||||
import { signUp } from '../utils'
|
||||
import Form from './Form.svelte'
|
||||
@ -50,6 +50,7 @@
|
||||
|
||||
if (result !== undefined) {
|
||||
setMetadata(presentation.metadata.Token, result.token)
|
||||
setMetadataLocalStorage(login.metadata.LastToken, result.token)
|
||||
const loc = getCurrentLocation()
|
||||
loc.path[1] = 'confirmationSend'
|
||||
loc.path.length = 2
|
||||
|
@ -48,12 +48,6 @@ export default mergeIds(loginId, login, {
|
||||
WantAnotherWorkspace: '' as IntlString,
|
||||
NotSeeingWorkspace: '' 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,
|
||||
Recover: '' 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)
|
||||
|
||||
if (accountsUrl === undefined) {
|
||||
@ -128,7 +130,7 @@ export async function createWorkspace (workspace: string): Promise<[Status, Logi
|
||||
if (overrideToken !== undefined) {
|
||||
const endpoint = getMetadata(login.metadata.OverrideEndpoint)
|
||||
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 = {
|
||||
method: 'createWorkspace',
|
||||
params: [workspace]
|
||||
params: [workspaceName]
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
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) {
|
||||
const loc = getCurrentLocation()
|
||||
loc.path[1] = 'login'
|
||||
loc.path.length = 2
|
||||
navigate(loc)
|
||||
if (doNavigate) {
|
||||
const loc = getCurrentLocation()
|
||||
loc.path[1] = 'login'
|
||||
loc.path.length = 2
|
||||
navigate(loc)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -26,7 +26,8 @@ export const loginId = 'login' as Plugin
|
||||
* @public
|
||||
*/
|
||||
export interface Workspace {
|
||||
workspace: string
|
||||
workspace: string //
|
||||
workspaceName?: string // A company name
|
||||
}
|
||||
|
||||
/**
|
||||
@ -50,6 +51,7 @@ export default plugin(loginId, {
|
||||
metadata: {
|
||||
AccountsUrl: '' as Asset,
|
||||
LoginTokens: '' as Metadata<Record<string, string>>,
|
||||
LastToken: '' as Metadata<string>,
|
||||
LoginEndpoint: '' as Metadata<string>,
|
||||
LoginEmail: '' as Metadata<string>,
|
||||
OverrideLoginToken: '' as Metadata<string>, // debug purposes
|
||||
|
@ -98,6 +98,7 @@
|
||||
setMetadataLocalStorage(login.metadata.LoginTokens, tokens)
|
||||
}
|
||||
setMetadata(presentation.metadata.Token, null)
|
||||
setMetadataLocalStorage(login.metadata.LastToken, null)
|
||||
setMetadataLocalStorage(login.metadata.LoginEndpoint, null)
|
||||
setMetadataLocalStorage(login.metadata.LoginEmail, null)
|
||||
void closeClient()
|
||||
|
@ -17,7 +17,7 @@
|
||||
export let workspace: string
|
||||
</script>
|
||||
|
||||
<div class="antiLogo red" class:mini>{workspace?.toUpperCase()?.[0]}</div>
|
||||
<div class="antiLogo red" class:mini>{workspace?.toUpperCase()?.[0] ?? ''}</div>
|
||||
|
||||
<style lang="scss">
|
||||
.antiLogo {
|
||||
|
@ -136,7 +136,7 @@
|
||||
<!-- <div class="drag"><Drag size={'small'} /></div> -->
|
||||
<!-- <div class="logo empty" /> -->
|
||||
<!-- <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> -->
|
||||
<!-- </div> -->
|
||||
<div class="ap-check">
|
||||
|
@ -32,10 +32,14 @@
|
||||
let admin = false
|
||||
onDestroy(
|
||||
ticker.subscribe(() => {
|
||||
void fetch(endpoint + `/api/v1/statistics?token=${token}`, {}).then(async (json) => {
|
||||
data = await json.json()
|
||||
admin = data?.admin ?? false
|
||||
})
|
||||
void fetch(endpoint + `/api/v1/statistics?token=${token}`, {})
|
||||
.then(async (json) => {
|
||||
data = await json.json()
|
||||
admin = data?.admin ?? false
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err)
|
||||
})
|
||||
})
|
||||
)
|
||||
const tabs: TabItem[] = [
|
||||
|
@ -124,8 +124,9 @@
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
getResource(login.function.GetWorkspaces).then(async (getWorkspaceFn) => {
|
||||
void getResource(login.function.GetWorkspaces).then(async (getWorkspaceFn) => {
|
||||
$workspacesStore = await getWorkspaceFn()
|
||||
await updateWindowTitle(getLocation())
|
||||
})
|
||||
})
|
||||
|
||||
@ -188,8 +189,15 @@
|
||||
})
|
||||
)
|
||||
|
||||
let windowWorkspaceName = ''
|
||||
|
||||
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)
|
||||
if (docTitle !== undefined && docTitle !== '') {
|
||||
document.title = ws == null ? docTitle : `${docTitle} - ${ws}`
|
||||
@ -663,7 +671,7 @@
|
||||
showPopup(SelectWorkspaceMenu, {}, popupSpacePosition)
|
||||
}}
|
||||
>
|
||||
<Logo mini={appsMini} workspace={$resolvedLocationStore.path[1]} />
|
||||
<Logo mini={appsMini} workspace={windowWorkspaceName ?? $resolvedLocationStore.path[1]} />
|
||||
</div>
|
||||
<div class="topmenu-container clear-mins flex-no-shrink" class:mini={appsMini}>
|
||||
<AppItem
|
||||
|
@ -53,7 +53,7 @@
|
||||
</div>
|
||||
</FixedColumn>
|
||||
</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`}>
|
||||
<svelte:self metrics={v} name="{i}. {k}" level={level + 1} />
|
||||
</div>
|
||||
|
@ -1,12 +1,12 @@
|
||||
import client from '@hcengineering/client'
|
||||
import core, {
|
||||
type AccountClient,
|
||||
type Client,
|
||||
ClientConnectEvent,
|
||||
type Version,
|
||||
getCurrentAccount,
|
||||
setCurrentAccount,
|
||||
versionToString
|
||||
versionToString,
|
||||
type AccountClient,
|
||||
type Client,
|
||||
type Version
|
||||
} from '@hcengineering/core'
|
||||
import login, { loginId } from '@hcengineering/login'
|
||||
import { addEventListener, broadcastEvent, getMetadata, getResource, setMetadata } from '@hcengineering/platform'
|
||||
@ -217,6 +217,7 @@ function clearMetadata (ws: string): void {
|
||||
setMetadataLocalStorage(login.metadata.LoginTokens, tokens)
|
||||
}
|
||||
setMetadata(presentation.metadata.Token, null)
|
||||
setMetadataLocalStorage(login.metadata.LastToken, null)
|
||||
document.cookie =
|
||||
encodeURIComponent(presentation.metadata.Token.replaceAll(':', '-')) + '=' + encodeURIComponent('') + '; path=/'
|
||||
setMetadataLocalStorage(login.metadata.LoginEndpoint, null)
|
||||
|
@ -195,6 +195,7 @@ export function signOut (): void {
|
||||
setMetadataLocalStorage(login.metadata.LoginTokens, tokens)
|
||||
}
|
||||
setMetadata(presentation.metadata.Token, null)
|
||||
setMetadataLocalStorage(login.metadata.LastToken, null)
|
||||
setMetadataLocalStorage(login.metadata.LoginEndpoint, null)
|
||||
setMetadataLocalStorage(login.metadata.LoginEmail, null)
|
||||
void closeClient()
|
||||
|
@ -82,6 +82,12 @@ if (frontUrl === undefined) {
|
||||
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 cursorMaxTime = process.env.SERVER_CURSOR_MAXTIMEMS
|
||||
|
||||
@ -105,7 +111,8 @@ const shutdown = start(url, {
|
||||
indexParallel: 2,
|
||||
indexProcessing: 50,
|
||||
productId: '',
|
||||
enableCompression
|
||||
enableCompression,
|
||||
accountsUrl
|
||||
})
|
||||
|
||||
const close = (): void => {
|
||||
|
@ -188,6 +188,8 @@ export function start (
|
||||
indexParallel: number // 2
|
||||
|
||||
enableCompression?: boolean
|
||||
|
||||
accountsUrl: string
|
||||
}
|
||||
): () => Promise<void> {
|
||||
addLocation(serverAttachmentId, () => import('@hcengineering/server-attachment-resources'))
|
||||
@ -270,7 +272,7 @@ export function start (
|
||||
}
|
||||
|
||||
const pipelineFactory: PipelineFactory = (ctx, workspace, upgrade, broadcast) => {
|
||||
const wsMetrics = metrics.newChild('🧲 ' + workspace.name, {})
|
||||
const wsMetrics = metrics.newChild('🧲 session', {})
|
||||
const conf: DbConfiguration = {
|
||||
domains: {
|
||||
[DOMAIN_TX]: 'MongoTx',
|
||||
@ -357,6 +359,7 @@ export function start (
|
||||
port: opt.port,
|
||||
productId: opt.productId,
|
||||
serverFactory: opt.serverFactory,
|
||||
enableCompression: opt.enableCompression
|
||||
enableCompression: opt.enableCompression,
|
||||
accountsUrl: opt.accountsUrl
|
||||
})
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 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.
|
||||
|
@ -131,7 +131,7 @@ async function getUpdateBacklinksTxes (
|
||||
export async function channelHTMLPresenter (doc: Doc, control: TriggerControl): Promise<string> {
|
||||
const channel = doc as ChunterSpace
|
||||
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)
|
||||
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> {
|
||||
const person = doc as Person
|
||||
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)
|
||||
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> {
|
||||
const organization = doc as Organization
|
||||
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)
|
||||
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> {
|
||||
const product = doc as Product
|
||||
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)
|
||||
return `<a href="${link}">${product.name}</a>`
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ import { workbenchId } from '@hcengineering/workbench'
|
||||
export async function leadHTMLPresenter (doc: Doc, control: TriggerControl): Promise<string> {
|
||||
const lead = doc as Lead
|
||||
const front = getMetadata(serverCore.metadata.FrontUrl) ?? ''
|
||||
const 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)
|
||||
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> {
|
||||
const vacancy = doc as Vacancy
|
||||
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)
|
||||
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 front = getMetadata(serverCore.metadata.FrontUrl) ?? ''
|
||||
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)
|
||||
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 issueId = await getIssueId(issue, control)
|
||||
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)
|
||||
return `<a href="${link}">${issueId}</a> ${issue.title}`
|
||||
}
|
||||
|
@ -17,7 +17,7 @@
|
||||
import builder, { migrateOperations, getModelVersion } from '@hcengineering/model-all'
|
||||
import { randomBytes } from 'crypto'
|
||||
import { Db, MongoClient } from 'mongodb'
|
||||
import accountPlugin, { getAccount, getMethods, getWorkspace } from '..'
|
||||
import accountPlugin, { getAccount, getMethods, getWorkspaceByUrl } from '..'
|
||||
import { setMetadata } from '@hcengineering/platform'
|
||||
|
||||
const DB_NAME = 'test_accounts'
|
||||
@ -147,14 +147,14 @@ describe('server', () => {
|
||||
|
||||
// Check we had one
|
||||
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, '', {
|
||||
method: 'removeWorkspace',
|
||||
params: ['andrey', workspace]
|
||||
})
|
||||
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 () => {
|
||||
|
@ -27,6 +27,7 @@ import core, {
|
||||
AccountRole,
|
||||
concatLink,
|
||||
Data,
|
||||
generateId,
|
||||
getWorkspaceId,
|
||||
Ref,
|
||||
systemAccountEmail,
|
||||
@ -93,12 +94,14 @@ export interface Account {
|
||||
*/
|
||||
export interface Workspace {
|
||||
_id: ObjectId
|
||||
workspace: string
|
||||
organisation: string
|
||||
workspace: string // An uniq workspace name, Database names
|
||||
accounts: ObjectId[]
|
||||
productId: string
|
||||
disabled?: boolean
|
||||
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 }
|
||||
}
|
||||
/**
|
||||
* @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
|
||||
* @param db -
|
||||
* @param workspace -
|
||||
* @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 }))
|
||||
}
|
||||
|
||||
@ -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> {
|
||||
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)
|
||||
if (account === null) {
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotFound, { account: email }))
|
||||
@ -253,7 +278,7 @@ export async function selectWorkspace (
|
||||
db: Db,
|
||||
productId: string,
|
||||
token: string,
|
||||
workspace: string,
|
||||
workspaceUrl: string,
|
||||
allowAdmin: boolean = true
|
||||
): Promise<WorkspaceLoginInfo> {
|
||||
let { email } = decodeToken(token)
|
||||
@ -263,21 +288,25 @@ export async function selectWorkspace (
|
||||
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) {
|
||||
return {
|
||||
endpoint: getEndpoint(),
|
||||
email,
|
||||
token: generateToken(email, getWorkspaceId(workspace, productId), getExtra(accountInfo)),
|
||||
workspace,
|
||||
token: generateToken(email, getWorkspaceId(workspaceInfo.workspace, productId), getExtra(accountInfo)),
|
||||
workspace: workspaceUrl,
|
||||
productId
|
||||
}
|
||||
}
|
||||
|
||||
const workspaceInfo = await getWorkspace(db, productId, workspace)
|
||||
|
||||
if (workspaceInfo !== null) {
|
||||
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
|
||||
|
||||
@ -286,8 +315,8 @@ export async function selectWorkspace (
|
||||
const result = {
|
||||
endpoint: getEndpoint(),
|
||||
email,
|
||||
token: generateToken(email, getWorkspaceId(workspace, productId), getExtra(accountInfo)),
|
||||
workspace,
|
||||
token: generateToken(email, getWorkspaceId(workspaceInfo.workspace, productId), getExtra(accountInfo)),
|
||||
workspace: workspaceUrl,
|
||||
productId
|
||||
}
|
||||
return result
|
||||
@ -546,11 +575,11 @@ export async function createAccount (
|
||||
/**
|
||||
* @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())
|
||||
.map((it) => ({ ...it, productId }))
|
||||
.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()
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
@ -569,32 +689,36 @@ export async function createWorkspace (
|
||||
migrationOperation: [string, MigrateOperation][],
|
||||
db: Db,
|
||||
productId: string,
|
||||
workspace: string,
|
||||
organisation: string
|
||||
): Promise<string> {
|
||||
if ((await getWorkspace(db, productId, workspace)) !== null) {
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.WorkspaceAlreadyExists, { workspace }))
|
||||
}
|
||||
const result = await db
|
||||
.collection(WORKSPACE_COLLECTION)
|
||||
.insertOne({
|
||||
workspace,
|
||||
organisation,
|
||||
version,
|
||||
productId
|
||||
})
|
||||
.then((e) => e.insertedId.toHexString())
|
||||
const initWS = getMetadata(toolPlugin.metadata.InitWorkspace)
|
||||
if (initWS !== undefined) {
|
||||
if ((await getWorkspace(db, productId, initWS)) !== null) {
|
||||
await initModel(getTransactor(), getWorkspaceId(workspace, productId), txes, [])
|
||||
await cloneWorkspace(getTransactor(), getWorkspaceId(initWS, productId), getWorkspaceId(workspace, productId))
|
||||
await upgradeModel(getTransactor(), getWorkspaceId(workspace, productId), txes, migrationOperation)
|
||||
return result
|
||||
email: string,
|
||||
workspaceName: string,
|
||||
workspace?: string
|
||||
): Promise<{ workspaceInfo: Workspace, err?: any }> {
|
||||
// We need to search for duplicate workspaceUrl
|
||||
await searchPromise
|
||||
|
||||
// Safe generate workspace record.
|
||||
searchPromise = generateWorkspaceRecord(db, email, productId, version, workspaceName, workspace)
|
||||
|
||||
const workspaceInfo = await searchPromise
|
||||
try {
|
||||
const initWS = getMetadata(toolPlugin.metadata.InitWorkspace)
|
||||
const wsId = getWorkspaceId(workspaceInfo.workspace, productId)
|
||||
if (initWS !== undefined) {
|
||||
if ((await getWorkspaceById(db, productId, initWS)) !== null) {
|
||||
await initModel(getTransactor(), wsId, txes, [])
|
||||
await cloneWorkspace(
|
||||
getTransactor(),
|
||||
getWorkspaceId(initWS, productId),
|
||||
getWorkspaceId(workspaceInfo.workspace, productId)
|
||||
)
|
||||
await upgradeModel(getTransactor(), wsId, txes, migrationOperation)
|
||||
}
|
||||
}
|
||||
await initModel(getTransactor(), wsId, txes, migrationOperation)
|
||||
} catch (err: any) {
|
||||
return { workspaceInfo, err }
|
||||
}
|
||||
await initModel(getTransactor(), getWorkspaceId(workspace, productId), txes, migrationOperation)
|
||||
return result
|
||||
return { workspaceInfo }
|
||||
}
|
||||
|
||||
/**
|
||||
@ -606,13 +730,13 @@ export async function upgradeWorkspace (
|
||||
migrationOperation: [string, MigrateOperation][],
|
||||
productId: string,
|
||||
db: Db,
|
||||
workspace: string,
|
||||
workspaceUrl: string,
|
||||
logger: ModelLogger = consoleModelLogger,
|
||||
forceUpdate: boolean = true
|
||||
): Promise<string> {
|
||||
const ws = await getWorkspace(db, productId, workspace)
|
||||
const ws = await getWorkspaceByUrl(db, productId, workspaceUrl)
|
||||
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 (productId !== '' || ws.productId !== undefined) {
|
||||
@ -621,7 +745,7 @@ export async function upgradeWorkspace (
|
||||
}
|
||||
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(
|
||||
`${forceUpdate ? 'force-' : ''}upgrade from "${
|
||||
currentVersion?.version !== undefined ? versionToString(currentVersion.version) : ''
|
||||
@ -632,12 +756,12 @@ export async function upgradeWorkspace (
|
||||
return versionStr
|
||||
}
|
||||
await db.collection(WORKSPACE_COLLECTION).updateOne(
|
||||
{ workspace },
|
||||
{ workspace: workspaceUrl },
|
||||
{
|
||||
$set: { version }
|
||||
}
|
||||
)
|
||||
await upgradeModel(getTransactor(), getWorkspaceId(workspace, productId), txes, migrationOperation, logger)
|
||||
await upgradeModel(getTransactor(), getWorkspaceId(workspaceUrl, productId), txes, migrationOperation, logger)
|
||||
return versionStr
|
||||
}
|
||||
|
||||
@ -646,14 +770,12 @@ export async function upgradeWorkspace (
|
||||
*/
|
||||
export const createUserWorkspace =
|
||||
(version: Data<Version>, txes: Tx[], migrationOperation: [string, MigrateOperation][]) =>
|
||||
async (db: Db, productId: string, token: string, workspace: 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 }))
|
||||
}
|
||||
|
||||
async (db: Db, productId: string, token: string, workspaceName: string): Promise<LoginInfo> => {
|
||||
const { email, extra } = decodeToken(token)
|
||||
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) {
|
||||
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 }))
|
||||
}
|
||||
|
||||
if (info.lastWorkspace !== undefined) {
|
||||
if (info.lastWorkspace !== undefined && info.admin === false) {
|
||||
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) {
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.WorkspaceAlreadyExists, { workspace }))
|
||||
}
|
||||
try {
|
||||
await createWorkspace(version, txes, migrationOperation, db, productId, workspace, '')
|
||||
} catch (err: any) {
|
||||
const { workspaceInfo, err } = await createWorkspace(
|
||||
version,
|
||||
txes,
|
||||
migrationOperation,
|
||||
db,
|
||||
productId,
|
||||
email,
|
||||
workspaceName
|
||||
)
|
||||
|
||||
if (err != null) {
|
||||
console.error(err)
|
||||
// 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(
|
||||
{
|
||||
_id: ws._id
|
||||
_id: workspaceInfo._id
|
||||
},
|
||||
{ $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() } })
|
||||
|
||||
const initWS = getMetadata(toolPlugin.metadata.InitWorkspace)
|
||||
const shouldUpdateAccount = initWS !== undefined && (await getWorkspace(db, productId, initWS)) !== null
|
||||
await assignWorkspace(db, productId, email, workspace, shouldUpdateAccount)
|
||||
await setRole(email, workspace, productId, AccountRole.Owner)
|
||||
const shouldUpdateAccount = initWS !== undefined && (await getWorkspaceById(db, productId, initWS)) !== null
|
||||
await assignWorkspace(db, productId, email, workspaceInfo.workspace, shouldUpdateAccount)
|
||||
await setRole(email, workspaceInfo.workspace, productId, AccountRole.Owner)
|
||||
const result = {
|
||||
endpoint: getEndpoint(),
|
||||
email,
|
||||
token: generateToken(email, getWorkspaceId(workspace, productId), getExtra(info)),
|
||||
productId
|
||||
token: generateToken(email, getWorkspaceId(workspaceInfo.workspace, productId), getExtra(info)),
|
||||
productId,
|
||||
workspace: workspaceInfo.workspaceUrl
|
||||
}
|
||||
console.log(`Creating workspace ${workspace} Done`)
|
||||
console.log(`Creating workspace "${workspaceName}" Done`)
|
||||
return result
|
||||
}
|
||||
|
||||
@ -720,7 +847,7 @@ export async function getInviteLink (
|
||||
limit: number
|
||||
): Promise<ObjectId> {
|
||||
const { workspace } = decodeToken(token)
|
||||
const wsPromise = await getWorkspace(db, productId, workspace.name)
|
||||
const wsPromise = await getWorkspaceById(db, productId, workspace.name)
|
||||
if (wsPromise === null) {
|
||||
throw new PlatformError(
|
||||
new Status(Severity.ERROR, platform.status.WorkspaceNotFound, { workspace: workspace.name })
|
||||
@ -738,17 +865,17 @@ export async function getInviteLink (
|
||||
/**
|
||||
* @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
|
||||
return data
|
||||
return { ...data, workspace: ws.workspaceUrl ?? ws.workspace }
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 account = await getAccount(db, email)
|
||||
if (account === null) return []
|
||||
@ -759,19 +886,49 @@ export async function getUserWorkspaces (db: Db, productId: string, token: strin
|
||||
.toArray()
|
||||
)
|
||||
.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 (
|
||||
db: Db,
|
||||
productId: string,
|
||||
_email: string,
|
||||
workspace: string
|
||||
workspaceUrl: string
|
||||
): Promise<{ accountId: ObjectId, workspaceId: ObjectId }> {
|
||||
const email = cleanEmail(_email)
|
||||
const wsPromise = await getWorkspace(db, productId, workspace)
|
||||
const wsPromise = await getWorkspaceById(db, productId, workspaceUrl)
|
||||
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 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> {
|
||||
const email = cleanEmail(_email)
|
||||
const connection = await connect(getTransactor(), getWorkspaceId(workspace, productId), email)
|
||||
const connection = await connect(getTransactor(), getWorkspaceId(workspace, productId))
|
||||
try {
|
||||
const ops = new TxOperations(connection, core.account.System)
|
||||
|
||||
@ -811,24 +968,30 @@ export async function assignWorkspace (
|
||||
db: Db,
|
||||
productId: string,
|
||||
_email: string,
|
||||
workspace: string,
|
||||
workspaceId: string,
|
||||
shouldReplaceAccount: boolean = false
|
||||
): Promise<void> {
|
||||
const email = cleanEmail(_email)
|
||||
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, {}))
|
||||
}
|
||||
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 })
|
||||
|
||||
if (account !== null) await createPersonAccount(account, productId, workspace, shouldReplaceAccount)
|
||||
if (account !== null) {
|
||||
await createPersonAccount(account, productId, workspaceId, shouldReplaceAccount)
|
||||
}
|
||||
|
||||
// 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
|
||||
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>> {
|
||||
@ -1090,10 +1253,10 @@ export async function checkJoin (
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export async function dropWorkspace (db: Db, productId: string, workspace: string): Promise<void> {
|
||||
const ws = await getWorkspace(db, productId, workspace)
|
||||
export async function dropWorkspace (db: Db, productId: string, workspaceId: string): Promise<void> {
|
||||
const ws = await getWorkspaceById(db, productId, workspaceId)
|
||||
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
|
||||
@ -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 }))
|
||||
}
|
||||
|
||||
const workspace = await getWorkspace(db, productId, tokenData.workspace.name)
|
||||
const workspace = await getWorkspaceById(db, productId, tokenData.workspace.name)
|
||||
if (workspace === null) {
|
||||
throw new PlatformError(
|
||||
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 }))
|
||||
}
|
||||
|
||||
const workspace = await getWorkspace(db, productId, tokenData.workspace.name)
|
||||
const workspace = await getWorkspaceById(db, productId, tokenData.workspace.name)
|
||||
if (workspace === null) {
|
||||
throw new PlatformError(
|
||||
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> {
|
||||
const connection = await connect(getTransactor(), getWorkspaceId(workspace, productId), email)
|
||||
const connection = await connect(getTransactor(), getWorkspaceId(workspace, productId))
|
||||
try {
|
||||
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)
|
||||
.then((result) => ({ id: request.id, result }))
|
||||
.catch((err) => {
|
||||
console.error(err)
|
||||
const status =
|
||||
err instanceof PlatformError
|
||||
? err.status
|
||||
: new Status(Severity.ERROR, platform.status.InternalServerError, {})
|
||||
if (status.code === platform.status.InternalServerError) {
|
||||
console.error(status, err)
|
||||
} else {
|
||||
console.error(status)
|
||||
}
|
||||
return {
|
||||
error:
|
||||
err instanceof PlatformError
|
||||
? err.status
|
||||
: new Status(Severity.ERROR, platform.status.InternalServerError, {})
|
||||
error: status
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -1272,6 +1440,7 @@ export function getMethods (
|
||||
getUserWorkspaces: wrap(getUserWorkspaces),
|
||||
getInviteLink: wrap(getInviteLink),
|
||||
getAccountInfo: wrap(getAccountInfo),
|
||||
getWorkspaceInfo: wrap(getWorkspaceInfo),
|
||||
createAccount: wrap(createAccount),
|
||||
createWorkspace: wrap(createUserWorkspace(version, txes, migrateOperations)),
|
||||
assignWorkspace: wrap(assignWorkspace),
|
||||
|
@ -54,6 +54,7 @@ import core, {
|
||||
TxWorkspaceEvent,
|
||||
WorkspaceEvent,
|
||||
WorkspaceId,
|
||||
WorkspaceIdWithUrl,
|
||||
generateId
|
||||
} from '@hcengineering/core'
|
||||
import { MinioService } from '@hcengineering/minio'
|
||||
@ -92,7 +93,7 @@ export interface DbConfiguration {
|
||||
adapters: Record<string, DbAdapterConfiguration>
|
||||
domains: Record<string, string>
|
||||
defaultAdapter: string
|
||||
workspace: WorkspaceId
|
||||
workspace: WorkspaceIdWithUrl
|
||||
metrics: MeasureContext
|
||||
fulltextAdapter: {
|
||||
factory: FullTextAdapterFactory
|
||||
@ -121,7 +122,7 @@ class TServerStorage implements ServerStorage {
|
||||
private readonly fulltextAdapter: FullTextAdapter,
|
||||
readonly storageAdapter: MinioService | undefined,
|
||||
readonly modelDb: ModelDb,
|
||||
private readonly workspace: WorkspaceId,
|
||||
private readonly workspace: WorkspaceIdWithUrl,
|
||||
readonly indexFactory: (storage: ServerStorage) => FullTextIndex,
|
||||
readonly options: ServerStorageOptions,
|
||||
metrics: MeasureContext,
|
||||
|
@ -37,7 +37,8 @@ import {
|
||||
Tx,
|
||||
TxFactory,
|
||||
TxResult,
|
||||
WorkspaceId
|
||||
WorkspaceId,
|
||||
WorkspaceIdWithUrl
|
||||
} from '@hcengineering/core'
|
||||
import { MinioService } from '@hcengineering/minio'
|
||||
import type { Asset, Resource } from '@hcengineering/platform'
|
||||
@ -114,7 +115,7 @@ export interface Pipeline extends LowLevelStorage {
|
||||
*/
|
||||
export interface TriggerControl {
|
||||
ctx: MeasureContext
|
||||
workspace: WorkspaceId
|
||||
workspace: WorkspaceIdWithUrl
|
||||
txFactory: TxFactory
|
||||
findAll: Storage['findAll']
|
||||
findAllCtx: <T extends Doc>(
|
||||
|
@ -157,7 +157,7 @@ describe('mongo operations', () => {
|
||||
}
|
||||
},
|
||||
defaultContentAdapter: 'default',
|
||||
workspace: getWorkspaceId(dbId, ''),
|
||||
workspace: { ...getWorkspaceId(dbId, ''), workspaceName: '', workspaceUrl: '' },
|
||||
storageFactory: () => createNullStorageFactory()
|
||||
}
|
||||
const ctx = new MeasureMetricsContext('client', {})
|
||||
|
@ -88,7 +88,8 @@ describe('server', () => {
|
||||
sessionFactory: (token, pipeline, broadcast) => new ClientSession(broadcast, token, pipeline),
|
||||
port: 3335,
|
||||
productId: '',
|
||||
serverFactory: startHttpServer
|
||||
serverFactory: startHttpServer,
|
||||
accountsUrl: ''
|
||||
})
|
||||
|
||||
function connect (): WebSocket {
|
||||
@ -186,7 +187,8 @@ describe('server', () => {
|
||||
sessionFactory: (token, pipeline, broadcast) => new ClientSession(broadcast, token, pipeline),
|
||||
port: 3336,
|
||||
productId: '',
|
||||
serverFactory: startHttpServer
|
||||
serverFactory: startHttpServer,
|
||||
accountsUrl: ''
|
||||
})
|
||||
|
||||
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 type { Pipeline, SessionContext } from '@hcengineering/server-core'
|
||||
import { type Token } from '@hcengineering/server-token'
|
||||
// import WebSocket, { RawData } from 'ws'
|
||||
|
||||
import {
|
||||
LOGGING_ENABLED,
|
||||
@ -42,6 +41,11 @@ import {
|
||||
type Workspace
|
||||
} from './types'
|
||||
|
||||
interface WorkspaceLoginInfo {
|
||||
workspaceName?: string // A company name
|
||||
workspace: string
|
||||
}
|
||||
|
||||
function timeoutPromise (time: number): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, time)
|
||||
@ -154,45 +158,96 @@ class TSessionManager implements SessionManager {
|
||||
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 (
|
||||
baseCtx: MeasureContext,
|
||||
ws: ConnectionSocket,
|
||||
token: Token,
|
||||
rawToken: string,
|
||||
pipelineFactory: PipelineFactory,
|
||||
productId: string,
|
||||
sessionId?: string
|
||||
): Promise<{ session: Session, context: MeasureContext } | { upgrade: true }> {
|
||||
sessionId: string | undefined,
|
||||
accountsUrl: string
|
||||
): Promise<
|
||||
{ session: Session, context: MeasureContext, workspaceName: string } | { upgrade: true } | { error: any }
|
||||
> {
|
||||
return await baseCtx.with('📲 add-session', {}, async (ctx) => {
|
||||
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)
|
||||
await workspace?.closing
|
||||
workspace = this.workspaces.get(wsString)
|
||||
const workspaceName = workspaceInfo.workspaceName ?? workspaceInfo.workspaceUrl ?? workspaceInfo.workspace
|
||||
|
||||
if (workspace === undefined) {
|
||||
workspace = this.createWorkspace(baseCtx, pipelineFactory, token)
|
||||
workspace = this.createWorkspace(
|
||||
baseCtx,
|
||||
pipelineFactory,
|
||||
token,
|
||||
workspaceInfo.workspaceUrl ?? workspaceInfo.workspace,
|
||||
workspaceName
|
||||
)
|
||||
}
|
||||
|
||||
let pipeline: Pipeline
|
||||
if (token.extra?.model === 'upgrade') {
|
||||
if (workspace.upgrade) {
|
||||
pipeline = await ctx.with(
|
||||
'💤 wait ' + token.workspace.name,
|
||||
{},
|
||||
async () => await (workspace as Workspace).pipeline
|
||||
)
|
||||
pipeline = await ctx.with('💤 wait ' + workspaceName, {}, async () => await (workspace as Workspace).pipeline)
|
||||
} 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 {
|
||||
if (workspace.upgrade) {
|
||||
return { upgrade: true }
|
||||
}
|
||||
pipeline = await ctx.with(
|
||||
'💤 wait ' + token.workspace.name,
|
||||
{},
|
||||
async () => await (workspace as Workspace).pipeline
|
||||
)
|
||||
pipeline = await ctx.with('💤 wait ' + workspaceName, {}, async () => await (workspace as Workspace).pipeline)
|
||||
}
|
||||
|
||||
const session = this.createSession(token, pipeline)
|
||||
@ -211,7 +266,7 @@ class TSessionManager implements SessionManager {
|
||||
session.useCompression
|
||||
)
|
||||
}
|
||||
return { session, context: workspace.context }
|
||||
return { session, context: workspace.context, workspaceName }
|
||||
})
|
||||
}
|
||||
|
||||
@ -222,15 +277,18 @@ class TSessionManager implements SessionManager {
|
||||
wsString: string,
|
||||
workspace: Workspace,
|
||||
pipelineFactory: PipelineFactory,
|
||||
ws: ConnectionSocket
|
||||
ws: ConnectionSocket,
|
||||
workspaceUrl: string,
|
||||
workspaceName: string
|
||||
): Promise<Pipeline> {
|
||||
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.
|
||||
// Drop all existing clients
|
||||
await this.closeAll(wsString, workspace, 0, 'upgrade')
|
||||
// Wipe workspace and update values.
|
||||
workspace.workspaceName = workspaceName
|
||||
if (!workspace.upgrade) {
|
||||
// This is previous workspace, intended to be closed.
|
||||
workspace.id = generateId()
|
||||
@ -238,9 +296,14 @@ class TSessionManager implements SessionManager {
|
||||
workspace.upgrade = token.extra?.model === 'upgrade'
|
||||
}
|
||||
// Re-create pipeline.
|
||||
workspace.pipeline = pipelineFactory(ctx, token.workspace, true, (tx, targets) => {
|
||||
this.broadcastAll(workspace, tx, targets)
|
||||
})
|
||||
workspace.pipeline = pipelineFactory(
|
||||
ctx,
|
||||
{ ...token.workspace, workspaceUrl, workspaceName },
|
||||
true,
|
||||
(tx, targets) => {
|
||||
this.broadcastAll(workspace, tx, targets)
|
||||
}
|
||||
)
|
||||
return await workspace.pipeline
|
||||
}
|
||||
|
||||
@ -271,20 +334,32 @@ class TSessionManager implements SessionManager {
|
||||
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 context = ctx.newChild('🧲 ' + token.workspace.name, {})
|
||||
const context = ctx.newChild('🧲 session', {})
|
||||
const workspace: Workspace = {
|
||||
context,
|
||||
id: generateId(),
|
||||
pipeline: pipelineFactory(context, token.workspace, upgrade, (tx, targets) => {
|
||||
this.broadcastAll(workspace, tx, targets)
|
||||
}),
|
||||
pipeline: pipelineFactory(
|
||||
context,
|
||||
{ ...token.workspace, workspaceUrl, workspaceName },
|
||||
upgrade,
|
||||
(tx, targets) => {
|
||||
this.broadcastAll(workspace, tx, targets)
|
||||
}
|
||||
),
|
||||
sessions: new Map(),
|
||||
upgrade
|
||||
upgrade,
|
||||
workspaceName
|
||||
}
|
||||
if (LOGGING_ENABLED) console.time(token.workspace.name)
|
||||
if (LOGGING_ENABLED) console.timeLog(token.workspace.name, 'Creating Workspace:', workspace.id)
|
||||
if (LOGGING_ENABLED) console.time(workspaceName)
|
||||
if (LOGGING_ENABLED) console.timeLog(workspaceName, 'Creating Workspace:', workspace.id)
|
||||
this.workspaces.set(toWorkspaceString(token.workspace), workspace)
|
||||
return workspace
|
||||
}
|
||||
@ -498,7 +573,9 @@ class TSessionManager implements SessionManager {
|
||||
msg: any,
|
||||
workspace: string
|
||||
): Promise<void> {
|
||||
const userCtx = requestCtx.newChild('📞 client', {}) as SessionContext
|
||||
const userCtx = requestCtx.newChild('📞 client', {
|
||||
workspace: '🧲 ' + workspace
|
||||
}) as SessionContext
|
||||
userCtx.sessionId = service.sessionInstanceId ?? ''
|
||||
|
||||
// Calculate total number of clients
|
||||
@ -668,6 +745,7 @@ export function start (
|
||||
productId: string
|
||||
serverFactory: ServerFactory
|
||||
enableCompression?: boolean
|
||||
accountsUrl: string
|
||||
} & Partial<Timeouts>
|
||||
): () => Promise<void> {
|
||||
const sessions = new TSessionManager(ctx, opt.sessionFactory, {
|
||||
@ -682,6 +760,7 @@ export function start (
|
||||
opt.pipelineFactory,
|
||||
opt.port,
|
||||
opt.productId,
|
||||
opt.enableCompression ?? true
|
||||
opt.enableCompression ?? true,
|
||||
opt.accountsUrl
|
||||
)
|
||||
}
|
||||
|
@ -13,20 +13,20 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { type MeasureContext, generateId } from '@hcengineering/core'
|
||||
import { generateId, type MeasureContext } from '@hcengineering/core'
|
||||
import { UNAUTHORIZED } from '@hcengineering/platform'
|
||||
import { type Response, serialize } from '@hcengineering/rpc'
|
||||
import { type Token, decodeToken } from '@hcengineering/server-token'
|
||||
import { serialize, type Response } from '@hcengineering/rpc'
|
||||
import { decodeToken, type Token } from '@hcengineering/server-token'
|
||||
import compression from 'compression'
|
||||
import cors from 'cors'
|
||||
import express from 'express'
|
||||
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 {
|
||||
LOGGING_ENABLED,
|
||||
type ConnectionSocket,
|
||||
type HandleRequestFunction,
|
||||
LOGGING_ENABLED,
|
||||
type PipelineFactory,
|
||||
type SessionManager
|
||||
} from './types'
|
||||
@ -44,7 +44,8 @@ export function startHttpServer (
|
||||
pipelineFactory: PipelineFactory,
|
||||
port: number,
|
||||
productId: string,
|
||||
enableCompression: boolean
|
||||
enableCompression: boolean,
|
||||
accountsUrl: string
|
||||
): () => Promise<void> {
|
||||
if (LOGGING_ENABLED) console.log(`starting server on port ${port} ...`)
|
||||
|
||||
@ -151,7 +152,13 @@ export function startHttpServer (
|
||||
skipUTF8Validation: true
|
||||
})
|
||||
// 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 = []
|
||||
|
||||
const data = {
|
||||
@ -190,8 +197,20 @@ export function startHttpServer (
|
||||
ws.on('message', (msg: Buffer) => {
|
||||
buffer?.push(msg)
|
||||
})
|
||||
const session = await sessions.addSession(ctx, cs, token, pipelineFactory, productId, sessionId)
|
||||
if ('upgrade' in session) {
|
||||
const session = await sessions.addSession(
|
||||
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()
|
||||
return
|
||||
}
|
||||
@ -204,7 +223,7 @@ export function startHttpServer (
|
||||
buff = Buffer.concat(msg).toString()
|
||||
}
|
||||
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
|
||||
@ -218,9 +237,10 @@ export function startHttpServer (
|
||||
const b = buffer
|
||||
buffer = undefined
|
||||
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) => {
|
||||
const url = new URL('http://localhost' + (request.url ?? ''))
|
||||
@ -234,7 +254,7 @@ export function startHttpServer (
|
||||
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) {
|
||||
if (LOGGING_ENABLED) console.error('invalid token', err)
|
||||
wss.handleUpgrade(request, socket, head, (ws) => {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import {
|
||||
type WorkspaceIdWithUrl,
|
||||
type Class,
|
||||
type Doc,
|
||||
type DocumentQuery,
|
||||
@ -78,7 +79,7 @@ export type BroadcastCall = (
|
||||
*/
|
||||
export type PipelineFactory = (
|
||||
ctx: MeasureContext,
|
||||
ws: WorkspaceId,
|
||||
ws: WorkspaceIdWithUrl,
|
||||
upgrade: boolean,
|
||||
broadcast: BroadcastFunc
|
||||
) => Promise<Pipeline>
|
||||
@ -115,6 +116,8 @@ export interface Workspace {
|
||||
sessions: Map<string, { session: Session, socket: ConnectionSocket }>
|
||||
upgrade: boolean
|
||||
closing?: Promise<void>
|
||||
|
||||
workspaceName: string
|
||||
}
|
||||
|
||||
/**
|
||||
@ -130,10 +133,14 @@ export interface SessionManager {
|
||||
ctx: MeasureContext,
|
||||
ws: ConnectionSocket,
|
||||
token: Token,
|
||||
rawToken: string,
|
||||
pipelineFactory: PipelineFactory,
|
||||
productId: string,
|
||||
sessionId?: string
|
||||
) => Promise<{ session: Session, context: MeasureContext } | { upgrade: true }>
|
||||
sessionId: string | undefined,
|
||||
accountsUrl: string
|
||||
) => Promise<
|
||||
{ session: Session, context: MeasureContext, workspaceName: string } | { upgrade: true } | { error: any }
|
||||
>
|
||||
|
||||
broadcastAll: (workspace: Workspace, tx: Tx[], targets?: string[]) => void
|
||||
|
||||
@ -170,5 +177,6 @@ export type ServerFactory = (
|
||||
pipelineFactory: PipelineFactory,
|
||||
port: number,
|
||||
productId: string,
|
||||
enableCompression: boolean
|
||||
enableCompression: boolean,
|
||||
accountsUrl: string
|
||||
) => () => Promise<void>
|
||||
|
@ -37,7 +37,6 @@ services:
|
||||
links:
|
||||
- mongodb
|
||||
- minio
|
||||
- transactor
|
||||
ports:
|
||||
- 3003:3003
|
||||
environment:
|
||||
@ -85,6 +84,7 @@ services:
|
||||
- elastic
|
||||
- minio
|
||||
- rekoni
|
||||
- account
|
||||
ports:
|
||||
- 3334:3334
|
||||
environment:
|
||||
@ -99,6 +99,7 @@ services:
|
||||
- MINIO_SECRET_KEY=minioadmin
|
||||
- REKONI_URL=http://rekoni:4005
|
||||
- FRONT_URL=http://localhost:8083
|
||||
- ACCOUNTS_URL=http://account:3003
|
||||
collaborator:
|
||||
image: hardcoreeng/collaborator
|
||||
links:
|
||||
|
@ -7,10 +7,12 @@ docker compose -p sanity up -d --force-recreate --renew-anon-volumes
|
||||
./wait-elastic.sh 9201
|
||||
|
||||
# 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
|
||||
./tool.sh create-account user1 -f John -l Appleseed -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
|
||||
./tool.sh set-user-role user1 sanity-ws 1
|
||||
./tool.sh confirm-email user1
|
||||
|
@ -14,6 +14,7 @@ node ../dev/tool/bundle.js upgrade-workspace sanity-ws
|
||||
|
||||
# Re-assign user to workspace.
|
||||
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 --list
|
@ -17,6 +17,6 @@ test.describe('login test', () => {
|
||||
await loginPage.login(PlatformUser, '1234')
|
||||
|
||||
const selectWorkspacePage = new SelectWorkspacePage(page)
|
||||
await selectWorkspacePage.selectWorkspace('sanity-ws')
|
||||
await selectWorkspacePage.selectWorkspace('SanityTest')
|
||||
})
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user