diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index b3d7804168..0f130afa7e 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -10653,12 +10653,13 @@ packages: dev: false file:projects/account.tgz: - resolution: {integrity: sha512-HlxJJR7cADZNKLQaAHv0ecnw44cK/zfmS3fcQZrt91+BJnytc9MwLOjpetYtS6ktCXKOTdKp2DtYZubFDDl2zw==, tarball: file:projects/account.tgz} + resolution: {integrity: sha512-g8Jj5vatEBYxczIayNpyhAJypuGS17w8t4htZB0ljRlctQ9IQ69fMKfuVYDb8OTKMZuhuadDthRAqqsJevuxvA==, tarball: file:projects/account.tgz} name: '@rush-temp/account' version: 0.0.0 dependencies: '@rushstack/heft': 0.41.8 '@types/heft-jest': 1.0.2 + '@types/minio': 7.0.11 '@typescript-eslint/eslint-plugin': 5.7.0_c25e8c1f4f4f7aaed27aa6f9ce042237 '@typescript-eslint/parser': 5.7.0_eslint@7.32.0+typescript@4.5.4 eslint: 7.32.0 @@ -10667,6 +10668,7 @@ packages: eslint-plugin-node: 11.1.0_eslint@7.32.0 eslint-plugin-promise: 5.2.0_eslint@7.32.0 jwt-simple: 0.5.6 + minio: 7.0.25 mongodb: 4.2.2 prettier: 2.5.1 typescript: 4.5.4 @@ -11045,7 +11047,7 @@ packages: dev: false file:projects/dev-client-resources.tgz: - resolution: {integrity: sha512-hcwOdV5TgwA6yTNeZDtMwAaGEOYFO5VzqWTXk+sK+SPSiEN2yy6rb2Do1cfgL0wvSXLH/F2VT+nr1WAlI909lQ==, tarball: file:projects/dev-client-resources.tgz} + resolution: {integrity: sha512-1SUKqC2CJ2uWW0XbQeYN8DVtc2/6w0abs3IXzQ763VWWdkTVecBg6MjoD42LgH4HyrSL/u1J4AV8/o98Tbuslw==, tarball: file:projects/dev-client-resources.tgz} name: '@rush-temp/dev-client-resources' version: 0.0.0 dependencies: @@ -11364,7 +11366,7 @@ packages: dev: false file:projects/lead-resources.tgz_096c09b0b673a57c275d9767a12070b1: - resolution: {integrity: sha512-Bko+x/XDsWFrhZ4EfXCZ8GwS56DMgWA5t2tYGyPF7nlzIPY97oJWPZGc2z1T7olyx0bye+Od9Vk7qmZI3KuBkQ==, tarball: file:projects/lead-resources.tgz} + resolution: {integrity: sha512-W842udh6/87OvajsjespWfSRZhSUJZPkGDSHCaMjtSePN5T4nL2EkNGR66mIi1RW88oAoyd3VPx8MSZmUWnCGQ==, tarball: file:projects/lead-resources.tgz} id: file:projects/lead-resources.tgz name: '@rush-temp/lead-resources' version: 0.0.0 @@ -11425,7 +11427,7 @@ packages: dev: false file:projects/login-resources.tgz_096c09b0b673a57c275d9767a12070b1: - resolution: {integrity: sha512-MnzXjBUZFvM2+oyagvYkpisissgM0dTtcCgFApyoRq2lzlpXdrbybP9ujMBYliqJ7i4r6rk4Wk8HrRo/wfYfyg==, tarball: file:projects/login-resources.tgz} + resolution: {integrity: sha512-85HwyicQhgSqbvQ/Lb+/qMxRZmCxC71u7HaZMVcw92Fa3Mu3wEH0kB0JHyPjvMA/mgrs6uvJmPh5bJwQHhkB0Q==, tarball: file:projects/login-resources.tgz} id: file:projects/login-resources.tgz name: '@rush-temp/login-resources' version: 0.0.0 @@ -11568,7 +11570,7 @@ packages: dev: false file:projects/model-contact.tgz_typescript@4.5.4: - resolution: {integrity: sha512-ffJA0hXhTeEjKGbCFKTgYk9ch8JsAnZKmDOtyTSzE+tmIklA0Q1gWQGwqttPSgrGVQuy2UXkYXv1m7cnhBICxg==, tarball: file:projects/model-contact.tgz} + resolution: {integrity: sha512-GD5NmMEaz2KD79NdbNlQTbmuc98hxeV0cwV8xgRtCZnbeNrDw4h71yUik4vGNLsgha8ezy2oefrdi5aET1wbig==, tarball: file:projects/model-contact.tgz} id: file:projects/model-contact.tgz name: '@rush-temp/model-contact' version: 0.0.0 @@ -12004,7 +12006,7 @@ packages: dev: false file:projects/pod-account.tgz: - resolution: {integrity: sha512-LqJ4mBkjSWhBA9OiiMUgLeoUF9IniLh1xr/G3iXRHSm8Li6OssMSTZ733CAoWVNPchAMn5xBibLV5Mi6syer9w==, tarball: file:projects/pod-account.tgz} + resolution: {integrity: sha512-OULVsjJpuo9JVOV9Ru1HO3GYCxRIOHlYqJvdtEezTG6HcorbC6i3+cQat8fbdGV2LQ8Lpup55Q5k40t1OrEAIA==, tarball: file:projects/pod-account.tgz} name: '@rush-temp/pod-account' version: 0.0.0 dependencies: @@ -12018,6 +12020,7 @@ packages: '@types/node': 16.11.14 '@typescript-eslint/eslint-plugin': 5.7.0_c25e8c1f4f4f7aaed27aa6f9ce042237 '@typescript-eslint/parser': 5.7.0_eslint@7.32.0+typescript@4.5.4 + cross-env: 7.0.3 esbuild: 0.12.29 eslint: 7.32.0 eslint-config-standard-with-typescript: 21.0.1_ce2fa0c4dfa1c256100cababd749a13a diff --git a/dev/docker-compose.yaml b/dev/docker-compose.yaml index d4325d0fa7..c6bcc9e053 100644 --- a/dev/docker-compose.yaml +++ b/dev/docker-compose.yaml @@ -47,6 +47,9 @@ services: environment: - MONGO_URL=mongodb://mongodb:27017 - TRANSACTOR_URL=ws://localhost:3333 + - MINIO_ENDPOINT=minio + - MINIO_ACCESS_KEY=minioadmin + - MINIO_SECRET_KEY=minioadmin front: image: anticrm/front links: diff --git a/dev/tool/src/index.ts b/dev/tool/src/index.ts index 572f083ccf..fc32242c9a 100644 --- a/dev/tool/src/index.ts +++ b/dev/tool/src/index.ts @@ -25,16 +25,13 @@ import { listWorkspaces, listAccounts } from '@anticrm/account' -import contact, { combineName } from '@anticrm/contact' -import core, { TxOperations } from '@anticrm/core' import { program } from 'commander' import { Client } from 'minio' import { Db, MongoClient } from 'mongodb' -import { connect } from './connect' import { rebuildElastic } from './elastic' import { importXml } from './importer' import { clearTelegramHistory } from './telegram' -import { diffWorkspace, dumpWorkspace, initWorkspace, restoreWorkspace, upgradeWorkspace } from './workspace' +import { diffWorkspace, dumpWorkspace, restoreWorkspace, upgradeWorkspace } from './workspace' const mongodbUri = process.env.MONGO_URL if (mongodbUri === undefined) { @@ -109,35 +106,8 @@ program .description('assign workspace') .action(async (email: string, workspace: string, cmd) => { return await withDatabase(mongodbUri, async (db, client) => { - console.log(`retrieveing account from ${email}...`) - const account = await getAccount(db, email) - if (account === null) { - throw new Error('account not found') - } - console.log(`assigning user ${email} to ${workspace}...`) await assignWorkspace(db, email, workspace) - - console.log('connecting to transactor...') - const connection = await connect(transactorUrl, workspace) - const ops = new TxOperations(connection, core.account.System) - - const name = combineName(account.first, account.last) - - console.log('create user in target workspace...') - const employee = await ops.createDoc(contact.class.Employee, contact.space.Employee, { - name, - city: 'Mountain View', - channels: [] - }) - - console.log('create account in target workspace...') - await ops.createDoc(contact.class.EmployeeAccount, core.space.Model, { - email, - employee, - name - }) - await connection.close() }) }) @@ -158,7 +128,6 @@ program .action(async (workspace, cmd) => { return await withDatabase(mongodbUri, async (db) => { await createWorkspace(db, workspace, cmd.organization) - await initWorkspace(mongodbUri, workspace, transactorUrl, minio) }) }) diff --git a/dev/tool/src/workspace.ts b/dev/tool/src/workspace.ts index ebcff49160..fafbc53427 100644 --- a/dev/tool/src/workspace.ts +++ b/dev/tool/src/workspace.ts @@ -16,7 +16,7 @@ import contact from '@anticrm/contact' import core, { DOMAIN_TX, Tx } from '@anticrm/core' -import builder, { migrateOperations, createDeps } from '@anticrm/model-all' +import builder, { migrateOperations } from '@anticrm/model-all' import { existsSync } from 'fs' import { mkdir, open, readFile, writeFile } from 'fs/promises' import { Client } from 'minio' @@ -30,46 +30,6 @@ import { rebuildElastic } from './elastic' const txes = JSON.parse(JSON.stringify(builder.getTxes())) as Tx[] -/** - * @public - */ -export async function initWorkspace ( - mongoUrl: string, - dbName: string, - transactorUrl: string, - minio: Client -): Promise { - if (txes.some(tx => tx.objectSpace !== core.space.Model)) { - throw Error('Model txes must target only core.space.Model') - } - - const client = new MongoClient(mongoUrl) - try { - await client.connect() - const db = client.db(dbName) - - console.log('dropping database...') - await db.dropDatabase() - - console.log('creating model...') - const model = txes - const result = await db.collection(DOMAIN_TX).insertMany(model as Document[]) - console.log(`${result.insertedCount} model transactions inserted.`) - - console.log('creating data...') - const connection = await connect(transactorUrl, dbName) - await createDeps(connection) - await connection.close() - - console.log('create minio bucket') - if (!(await minio.bucketExists(dbName))) { - await minio.makeBucket(dbName, 'k8s') - } - } finally { - await client.close() - } -} - /** * @public */ @@ -79,7 +39,7 @@ export async function upgradeWorkspace ( transactorUrl: string, minio: Client ): Promise { - if (txes.some(tx => tx.objectSpace !== core.space.Model)) { + if (txes.some((tx) => tx.objectSpace !== core.space.Model)) { throw Error('Model txes must target only core.space.Model') } diff --git a/packages/ui/src/components/StylishEdit.svelte b/packages/ui/src/components/StylishEdit.svelte index ee215cc622..8bbc53f5d5 100644 --- a/packages/ui/src/components/StylishEdit.svelte +++ b/packages/ui/src/components/StylishEdit.svelte @@ -22,6 +22,7 @@ export let error: string | undefined = undefined export let password: boolean | undefined = undefined export let id: string | undefined = undefined + export let name: string | undefined = undefined
@@ -30,6 +31,7 @@ type="password" class:nolabel={!label} {id} + {name} bind:value on:blur on:change @@ -42,6 +44,7 @@ type="text" class:nolabel={!label} {id} + {name} bind:value on:blur on:change diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 8056ad0e3c..e1ffd65211 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -23,7 +23,7 @@ import Root from './components/internal/Root.svelte' export type { AnyComponent, AnySvelteComponent, Action, LabelAndProps, TooltipAligment } from './types' // export { applicationShortcutKey } from './utils' -export { getCurrentLocation, navigate, location } from './location' +export { getCurrentLocation, locationToUrl, navigate, location } from './location' export { default as EditBox } from './components/EditBox.svelte' export { default as Label } from './components/Label.svelte' @@ -106,16 +106,23 @@ interface CompAndProps { export const popupstore = writable([]) -export function showPopup (component: AnySvelteComponent | AnyComponent, props: any, element?: PopupAlignment, onClose?: (result: any) => void): void { +export function showPopup ( + component: AnySvelteComponent | AnyComponent, + props: any, + element?: PopupAlignment, + onClose?: (result: any) => void +): void { if (typeof component === 'string') { - getResource(component).then(resolved => { - popupstore.update(popups => { - popups.push({ is: resolved, props, element, onClose }) - return popups + getResource(component) + .then((resolved) => { + popupstore.update((popups) => { + popups.push({ is: resolved, props, element, onClose }) + return popups + }) }) - }).catch(err => console.log(err)) + .catch((err) => console.log(err)) } else { - popupstore.update(popups => { + popupstore.update((popups) => { popups.push({ is: component, props, element, onClose }) return popups }) @@ -123,7 +130,7 @@ export function showPopup (component: AnySvelteComponent | AnyComponent, props: } export function closePopup (): void { - popupstore.update(popups => { + popupstore.update((popups) => { popups.pop() return popups }) @@ -138,16 +145,37 @@ export const tooltipstore = writable({ anchor: undefined }) -export function showTooltip (label: IntlString | undefined, element: HTMLElement, direction?: TooltipAligment, component?: AnySvelteComponent | AnyComponent, props?: any, anchor?: HTMLElement): void { - tooltipstore.set({ label: label, element: element, direction: direction, component: component, props: props, anchor: anchor }) +export function showTooltip ( + label: IntlString | undefined, + element: HTMLElement, + direction?: TooltipAligment, + component?: AnySvelteComponent | AnyComponent, + props?: any, + anchor?: HTMLElement +): void { + tooltipstore.set({ + label: label, + element: element, + direction: direction, + component: component, + props: props, + anchor: anchor + }) } export function closeTooltip (): void { - tooltipstore.set({ label: undefined, element: undefined, direction: undefined, component: undefined, props: undefined, anchor: undefined }) + tooltipstore.set({ + label: undefined, + element: undefined, + direction: undefined, + component: undefined, + props: undefined, + anchor: undefined + }) } -export const ticker = readable(Date.now(), set => { - const interval = setInterval(() => { +export const ticker = readable(Date.now(), (set) => { + setInterval(() => { set(Date.now()) }, 10000) }) diff --git a/plugins/login-assets/lang/en.json b/plugins/login-assets/lang/en.json index df69fa5a1a..c34e44d996 100644 --- a/plugins/login-assets/lang/en.json +++ b/plugins/login-assets/lang/en.json @@ -1,7 +1,27 @@ { + "status": { + "RequiredField": "Required field {field}", + "FieldsDoNotMatch": "{field} don't match {field2}", + "ConnectingToServer": "Connecting to server....", + "IncorrectValue": "Incorrect value {field}" + }, "string": { "LogIn": "Login", "SignUp": "Sign Up", - "DoNotHaveAnAccount": "Do not have an account?" + "CreateWorkspace": "Create workspace", + "HaveWorkspace": "Already have a workspace?", + "LastName": "Last name", + "FirstName": "First name", + "Join": "Join", + "Email": "Email", + "Password": "Password", + "Workspace": "Workspace", + "DoNotHaveAnAccount": "Do not have an account?", + "PasswordRepeat": "Repeat password", + "HaveAccount": "Already have an account?", + "SelectWorkspace": "Select workspace", + "Copy": "Copy", + "Close": "Close", + "InviteDescription": "Share this link to invite other users" } } \ No newline at end of file diff --git a/plugins/login-resources/package.json b/plugins/login-resources/package.json index 8b3f98b38c..3e5bdcef40 100644 --- a/plugins/login-resources/package.json +++ b/plugins/login-resources/package.json @@ -33,6 +33,7 @@ "@anticrm/platform": "~0.6.5", "svelte": "^3.37.0", "@anticrm/login": "~0.6.1", + "@anticrm/account": "~0.6.0", "@anticrm/ui": "~0.6.0", "@anticrm/workbench": "~0.6.1" } diff --git a/plugins/login-resources/src/components/CreateWorkspaceForm.svelte b/plugins/login-resources/src/components/CreateWorkspaceForm.svelte new file mode 100644 index 0000000000..7e8a8c1ffe --- /dev/null +++ b/plugins/login-resources/src/components/CreateWorkspaceForm.svelte @@ -0,0 +1,68 @@ + + + +
{ + const loc = getCurrentLocation() + loc.path[1] = 'selectWorkspace' + loc.path.length = 2 + navigate(loc) + }} +/> diff --git a/plugins/login-resources/src/components/Form.svelte b/plugins/login-resources/src/components/Form.svelte index 1ec457daab..d8630324e2 100644 --- a/plugins/login-resources/src/components/Form.svelte +++ b/plugins/login-resources/src/components/Form.svelte @@ -1,5 +1,6 @@
- diff --git a/plugins/login-resources/src/components/Join.svelte b/plugins/login-resources/src/components/Join.svelte new file mode 100644 index 0000000000..59cb2c1473 --- /dev/null +++ b/plugins/login-resources/src/components/Join.svelte @@ -0,0 +1,100 @@ + + + +
{ + page = page === 'login' ? 'signUp' : 'login' + }} +/> diff --git a/plugins/login-resources/src/components/LoginApp.svelte b/plugins/login-resources/src/components/LoginApp.svelte index d7a9b426d8..bdec2a8e06 100644 --- a/plugins/login-resources/src/components/LoginApp.svelte +++ b/plugins/login-resources/src/components/LoginApp.svelte @@ -1,42 +1,64 @@ -
{#if page === 'login'} - page = event.detail}/> + {:else if page === 'signup'} - page = event.detail}/> + + {:else if page === 'createWorkspace'} + + {:else if page === 'selectWorkspace'} + + {:else if page === 'join'} + {/if}
- +

A unique place to manage all of your work

Welcome to the Platform

+
\ No newline at end of file + diff --git a/plugins/login-resources/src/components/LoginForm.svelte b/plugins/login-resources/src/components/LoginForm.svelte index 29270159ca..231de7e09c 100644 --- a/plugins/login-resources/src/components/LoginForm.svelte +++ b/plugins/login-resources/src/components/LoginForm.svelte @@ -1,72 +1,77 @@ - - { dispatch('switch', 'signup') }} + bottomActionFunc={() => { + const loc = getCurrentLocation() + loc.path[1] = 'signup' + loc.path.length = 2 + navigate(loc) + }} /> diff --git a/plugins/login-resources/src/components/LoginFormOld.svelte b/plugins/login-resources/src/components/LoginFormOld.svelte deleted file mode 100644 index de80e969e3..0000000000 --- a/plugins/login-resources/src/components/LoginFormOld.svelte +++ /dev/null @@ -1,146 +0,0 @@ - - - -{#await loginCheck then value} - {#if loginActive} - - {:else} - - {/if} -{/await} - - diff --git a/plugins/login-resources/src/components/MainLoginForm.svelte b/plugins/login-resources/src/components/MainLoginForm.svelte deleted file mode 100644 index 331d7576b1..0000000000 --- a/plugins/login-resources/src/components/MainLoginForm.svelte +++ /dev/null @@ -1,45 +0,0 @@ - - - - diff --git a/plugins/login-resources/src/components/SelectWorkspace.svelte b/plugins/login-resources/src/components/SelectWorkspace.svelte new file mode 100644 index 0000000000..1f40fda391 --- /dev/null +++ b/plugins/login-resources/src/components/SelectWorkspace.svelte @@ -0,0 +1,138 @@ + + + + +
+
+
+ +
+ {#await getWorkspaces() then workspaces} +
+ {#each workspaces as workspace (workspace._id)} +
select(workspace.workspace)} + > + {workspace.workspace} +
+ {/each} + {#if !workspaces.length} +
+
+ {/if} +
+
+ {#if workspaces.length} + + {/if} + {/await} + + + diff --git a/plugins/login-resources/src/components/SignupForm.svelte b/plugins/login-resources/src/components/SignupForm.svelte index fa855b2b4a..f50f77fb10 100644 --- a/plugins/login-resources/src/components/SignupForm.svelte +++ b/plugins/login-resources/src/components/SignupForm.svelte @@ -1,68 +1,78 @@ - -
{ dispatch('switch', 'login') }} + { + const loc = getCurrentLocation() + loc.path[1] = 'login' + loc.path.length = 2 + navigate(loc) + }} /> diff --git a/plugins/login-resources/src/components/StatusControl.svelte b/plugins/login-resources/src/components/StatusControl.svelte index c375cad6f7..99a53bcc07 100644 --- a/plugins/login-resources/src/components/StatusControl.svelte +++ b/plugins/login-resources/src/components/StatusControl.svelte @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. --> - {#if status.severity !== Severity.OK} -
- -
+
+ +
{/if} \ No newline at end of file + diff --git a/plugins/login-resources/src/index.ts b/plugins/login-resources/src/index.ts index 49b34ee85a..cfbafec21d 100644 --- a/plugins/login-resources/src/index.ts +++ b/plugins/login-resources/src/index.ts @@ -26,5 +26,5 @@ import LoginApp from './components/LoginApp.svelte' export default async () => ({ component: { LoginApp - }, + } }) diff --git a/plugins/login-resources/src/plugin.ts b/plugins/login-resources/src/plugin.ts index 604a3d6a9c..0efe1b0b61 100644 --- a/plugins/login-resources/src/plugin.ts +++ b/plugins/login-resources/src/plugin.ts @@ -16,21 +16,33 @@ import type { StatusCode, IntlString } from '@anticrm/platform' import { mergeIds } from '@anticrm/platform' -import type { AnyComponent } from '@anticrm/ui' import login, { loginId } from '@anticrm/login' export default mergeIds(loginId, login, { status: { RequiredField: '' as StatusCode<{ field: string }>, - ConnectingToServer: '' as StatusCode + FieldsDoNotMatch: '' as StatusCode<{ field: string, field2: string }>, + ConnectingToServer: '' as StatusCode, + IncorrectValue: '' as StatusCode<{ field: string }> }, string: { + CreateWorkspace: '' as IntlString, + HaveWorkspace: '' as IntlString, + LastName: '' as IntlString, + FirstName: '' as IntlString, + HaveAccount: '' as IntlString, + Join: '' as IntlString, Email: '' as IntlString, Password: '' as IntlString, + PasswordRepeat: '' as IntlString, Workspace: '' as IntlString, LogIn: '' as IntlString, SignUp: '' as IntlString, - DoNotHaveAnAccount: '' as IntlString + SelectWorkspace: '' as IntlString, + DoNotHaveAnAccount: '' as IntlString, + Copy: '' as IntlString, + Close: '' as IntlString, + InviteDescription: '' as IntlString } }) diff --git a/plugins/login-resources/src/utils.ts b/plugins/login-resources/src/utils.ts index f0f3de04f6..afc23b1f1d 100644 --- a/plugins/login-resources/src/utils.ts +++ b/plugins/login-resources/src/utils.ts @@ -13,12 +13,14 @@ // limitations under the License. // -import { Status, OK, unknownError, getMetadata, serialize } from '@anticrm/platform' +import { Status, OK, unknownError, getMetadata, serialize, unknownStatus } from '@anticrm/platform' import type { Request, Response } from '@anticrm/platform' +import type { Workspace } from '@anticrm/account' import login from '@anticrm/login' +import { fetchMetadataLocalStorage, getCurrentLocation, navigate } from '@anticrm/ui' -export interface LoginInfo { +export interface LoginInfo { token: string endpoint: string email: string @@ -27,11 +29,7 @@ export interface LoginInfo { /** * Perform a login operation to required workspace with user credentials. */ -export async function doLogin ( - email: string, - password: string, - workspace: string -): Promise<[Status, LoginInfo | undefined]> { +export async function doLogin (email: string, password: string): Promise<[Status, LoginInfo | undefined]> { const accountsUrl = getMetadata(login.metadata.AccountsUrl) if (accountsUrl === undefined) { @@ -46,9 +44,9 @@ export async function doLogin ( } } - const request: Request<[string, string, string]> = { + const request: Request<[string, string]> = { method: 'login', - params: [email, password, workspace] + params: [email, password] } try { @@ -67,3 +65,277 @@ export async function doLogin ( return [unknownError(err), undefined] } } + +export async function signUp ( + email: string, + password: string, + first: string, + last: string +): Promise<[Status, LoginInfo | undefined]> { + const accountsUrl = getMetadata(login.metadata.AccountsUrl) + + if (accountsUrl === undefined) { + throw new Error('accounts url not specified') + } + + const token = getMetadata(login.metadata.OverrideLoginToken) + if (token !== undefined) { + const endpoint = getMetadata(login.metadata.OverrideEndpoint) + if (endpoint !== undefined) { + return [OK, { token, endpoint, email }] + } + } + + const request: Request<[string, string, string, string]> = { + method: 'createAccount', + params: [email, password, first, last] + } + + try { + const response = await fetch(accountsUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: serialize(request) + }) + const result: Response = await response.json() + return [result.error ?? OK, result.result] + } catch (err) { + return [unknownError(err), undefined] + } +} + +export async function createWorkspace (workspace: string): Promise<[Status, LoginInfo | undefined]> { + const accountsUrl = getMetadata(login.metadata.AccountsUrl) + + if (accountsUrl === undefined) { + throw new Error('accounts url not specified') + } + + const overrideToken = getMetadata(login.metadata.OverrideLoginToken) + const email = getMetadata(login.metadata.LoginEmail) ?? '' + if (overrideToken !== undefined) { + const endpoint = getMetadata(login.metadata.OverrideEndpoint) + if (endpoint !== undefined) { + return [OK, { token: overrideToken, endpoint, email }] + } + } + + const token = fetchMetadataLocalStorage(login.metadata.LoginToken) + if (token === null) { + const loc = getCurrentLocation() + loc.path[1] = 'login' + loc.path.length = 2 + navigate(loc) + return [unknownStatus('Please login'), undefined] + } + + const request: Request<[string]> = { + method: 'createWorkspace', + params: [workspace] + } + + try { + const response = await fetch(accountsUrl, { + method: 'POST', + headers: { + Authorization: 'Bearer ' + token, + 'Content-Type': 'application/json' + }, + body: serialize(request) + }) + const result: Response = await response.json() + return [result.error ?? OK, result.result] + } catch (err) { + return [unknownError(err), undefined] + } +} + +export async function getWorkspaces (): Promise { + const accountsUrl = getMetadata(login.metadata.AccountsUrl) + + if (accountsUrl === undefined) { + throw new Error('accounts url not specified') + } + + const token = fetchMetadataLocalStorage(login.metadata.LoginToken) + if (token === null) { + const loc = getCurrentLocation() + loc.path[1] = 'login' + loc.path.length = 2 + navigate(loc) + return [] + } + + const request: Request<[]> = { + method: 'getUserWorkspaces', + params: [] + } + + try { + const response = await fetch(accountsUrl, { + method: 'POST', + headers: { + Authorization: 'Bearer ' + token, + 'Content-Type': 'application/json' + }, + body: serialize(request) + }) + const result: Response = await response.json() + console.log(result) + return result.result + } catch (err) { + return [] + } +} + +export async function selectWorkspace (workspace: string): Promise<[Status, LoginInfo | undefined]> { + const accountsUrl = getMetadata(login.metadata.AccountsUrl) + + if (accountsUrl === undefined) { + throw new Error('accounts url not specified') + } + + const token = fetchMetadataLocalStorage(login.metadata.LoginToken) + if (token === null) { + const loc = getCurrentLocation() + loc.path[1] = 'login' + loc.path.length = 2 + navigate(loc) + return [unknownStatus('Please login'), undefined] + } + + const request: Request<[string]> = { + method: 'selectWorkspace', + params: [workspace] + } + + try { + const response = await fetch(accountsUrl, { + method: 'POST', + headers: { + Authorization: 'Bearer ' + token, + 'Content-Type': 'application/json' + }, + body: serialize(request) + }) + const result: Response = await response.json() + return [result.error ?? OK, result.result] + } catch (err) { + return [unknownError(err), undefined] + } +} + +export async function getWorkspaceHash (): Promise { + const accountsUrl = getMetadata(login.metadata.AccountsUrl) + + if (accountsUrl === undefined) { + throw new Error('accounts url not specified') + } + + const token = fetchMetadataLocalStorage(login.metadata.LoginToken) + if (token === null) { + const loc = getCurrentLocation() + loc.path[1] = 'login' + loc.path.length = 2 + navigate(loc) + return '' + } + + const request: Request<[]> = { + method: 'getWorkspaceHash', + params: [] + } + + const response = await fetch(accountsUrl, { + method: 'POST', + headers: { + Authorization: 'Bearer ' + token, + 'Content-Type': 'application/json' + }, + body: serialize(request) + }) + const result: Response = await response.json() + return result.result +} + +export async function join ( + email: string, + password: string, + workspaceHash: string +): Promise<[Status, LoginInfo | undefined]> { + const accountsUrl = getMetadata(login.metadata.AccountsUrl) + + if (accountsUrl === undefined) { + throw new Error('accounts url not specified') + } + + const token = getMetadata(login.metadata.OverrideLoginToken) + if (token !== undefined) { + const endpoint = getMetadata(login.metadata.OverrideEndpoint) + if (endpoint !== undefined) { + return [OK, { token, endpoint, email }] + } + } + + const request: Request<[string, string, string]> = { + method: 'join', + params: [email, password, workspaceHash] + } + + try { + const response = await fetch(accountsUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: serialize(request) + }) + const result: Response = await response.json() + return [result.error ?? OK, result.result] + } catch (err) { + return [unknownError(err), undefined] + } +} + +export async function signUpJoin ( + email: string, + password: string, + first: string, + last: string, + workspaceHash: string +): Promise<[Status, LoginInfo | undefined]> { + const accountsUrl = getMetadata(login.metadata.AccountsUrl) + + if (accountsUrl === undefined) { + throw new Error('accounts url not specified') + } + + const token = getMetadata(login.metadata.OverrideLoginToken) + if (token !== undefined) { + const endpoint = getMetadata(login.metadata.OverrideEndpoint) + if (endpoint !== undefined) { + return [OK, { token, endpoint, email }] + } + } + + const request: Request<[string, string, string, string, string]> = { + method: 'signUpJoin', + params: [email, password, first, last, workspaceHash] + } + + try { + const response = await fetch(accountsUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: serialize(request) + }) + const result: Response = await response.json() + return [result.error ?? OK, result.result] + } catch (err) { + return [unknownError(err), undefined] + } +} diff --git a/pods/account/kube/deployment.yml b/pods/account/kube/deployment.yml index 866aea3a0e..5ed8536c44 100644 --- a/pods/account/kube/deployment.yml +++ b/pods/account/kube/deployment.yml @@ -24,3 +24,20 @@ spec: secretKeyRef: name: mongodb key: url + - name: TRANSACTOR_URL + value: ws://transactor/ + - name: MINIO_ENDPOINT + valueFrom: + secretKeyRef: + name: minio + key: endpoint + - name: MINIO_ACCESS_KEY + valueFrom: + secretKeyRef: + name: minio + key: accessKey + - name: MINIO_SECRET_KEY + valueFrom: + secretKeyRef: + name: minio + key: secretKey diff --git a/pods/account/package.json b/pods/account/package.json index 3b5b16546f..4be78dfc6f 100644 --- a/pods/account/package.json +++ b/pods/account/package.json @@ -12,10 +12,12 @@ "bundle": "esbuild src/index.ts --bundle --minify --platform=node > bundle.js", "docker:build": "docker build -t anticrm/account .", "docker:push": "docker push anticrm/account", + "run-local": "cross-env MONGO_URL=mongodb://localhost:27017 MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin MINIO_ENDPOINT=localhost TRANSACTOR_URL=ws:/localhost:3333 ts-node src/index.ts", "lint": "eslint src", "format": "prettier --write src && eslint --fix src" }, "devDependencies": { + "cross-env": "^7.0.3", "@anticrm/platform-rig": "~0.6.0", "@types/heft-jest": "^1.0.2", "@types/node": "^16.4.10", @@ -32,6 +34,7 @@ "@typescript-eslint/parser": "^5.4.0", "eslint-config-standard-with-typescript": "^21.0.1", "prettier": "^2.4.1", + "ts-node": "^10.2.1", "@rushstack/heft": "^0.41.1", "typescript": "^4.3.5" }, diff --git a/pods/account/src/index.ts b/pods/account/src/index.ts index cd3763fe11..fda029efbb 100644 --- a/pods/account/src/index.ts +++ b/pods/account/src/index.ts @@ -17,6 +17,7 @@ import accountPlugin, { ACCOUNT_DB, methods } from '@anticrm/account' import platform, { Request, Response, serialize, setMetadata, Severity, Status } from '@anticrm/platform' import cors from '@koa/cors' +import { IncomingHttpHeaders } from 'http' import Koa from 'koa' import bodyParser from 'koa-bodyparser' import Router from 'koa-router' @@ -41,9 +42,21 @@ let client: MongoClient const app = new Koa() const router = new Router() +const extractToken = (header: IncomingHttpHeaders): string | undefined => { + try { + return header.authorization?.slice(7) ?? undefined + } catch { + return undefined + } +} + router.post('rpc', '/', async (ctx) => { + const token = extractToken(ctx.request.headers) + const request = ctx.request.body - const method = (methods as { [key: string]: (db: Db, request: Request) => Response })[request.method] + const method = (methods as { [key: string]: (db: Db, request: Request, token?: string) => Response })[ + request.method + ] if (method === undefined) { const response: Response = { id: request.id, @@ -57,7 +70,7 @@ router.post('rpc', '/', async (ctx) => { client = await MongoClient.connect(dbUri) } const db = client.db(ACCOUNT_DB) - const result = await method(db, request) + const result = await method(db, request, token) console.log(result) ctx.body = result }) diff --git a/server/account/package.json b/server/account/package.json index 6f0821a415..2821a8a639 100644 --- a/server/account/package.json +++ b/server/account/package.json @@ -23,11 +23,18 @@ "eslint-config-standard-with-typescript": "^21.0.1", "prettier": "^2.4.1", "@rushstack/heft": "^0.41.1", - "typescript": "^4.3.5" + "typescript": "^4.3.5", + "@types/minio": "^7.0.10" }, "dependencies": { "mongodb": "^4.1.1", "@anticrm/platform": "~0.6.5", + "@anticrm/model-all": "~0.6.0", + "minio": "^7.0.19", + "@anticrm/core": "~0.6.14", + "@anticrm/contact": "~0.6.2", + "@anticrm/client-resources": "~0.6.4", + "@anticrm/client": "~0.6.1", "jwt-simple": "~0.5.6" } } diff --git a/server/account/src/connect.ts b/server/account/src/connect.ts new file mode 100644 index 0000000000..d99f8c3dc4 --- /dev/null +++ b/server/account/src/connect.ts @@ -0,0 +1,36 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// Copyright © 2021, 2022 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import client from '@anticrm/client' +import clientResources from '@anticrm/client-resources' +import { Client } from '@anticrm/core' +import { getMetadata, setMetadata } from '@anticrm/platform' +import { encode } from 'jwt-simple' +import accountPlugin from '.' + +// eslint-disable-next-line +const WebSocket = require('ws') + +export async function connect (transactorUrl: string, workspace: string, email?: string): Promise { + const token = encode( + { email: email ?? 'anticrm@hc.engineering', workspace }, + getMetadata(accountPlugin.metadata.Secret) ?? 'secret' + ) + + // We need to override default factory with 'ws' one. + setMetadata(client.metadata.ClientSocketFactory, (url) => new WebSocket(url)) + return await (await clientResources()).function.GetClient(token, transactorUrl) +} diff --git a/server/account/src/index.ts b/server/account/src/index.ts index ed1896253b..4e3b424a57 100644 --- a/server/account/src/index.ts +++ b/server/account/src/index.ts @@ -14,10 +14,25 @@ // limitations under the License. // -import platform, { getMetadata, Metadata, Plugin, Request, Response, PlatformError, plugin, Severity, Status, StatusCode } from '@anticrm/platform' +import platform, { + getMetadata, + Metadata, + Plugin, + Request, + Response, + PlatformError, + plugin, + Severity, + Status, + StatusCode +} from '@anticrm/platform' import { pbkdf2Sync, randomBytes } from 'crypto' -import { encode } from 'jwt-simple' +import { decode, encode } from 'jwt-simple' import { Binary, Db, ObjectId } from 'mongodb' +import core, { TxOperations } from '@anticrm/core' +import contact, { combineName } from '@anticrm/contact' +import { connect } from './connect' +import { initWorkspace } from './tool' const WORKSPACE_COLLECTION = 'workspace' const ACCOUNT_COLLECTION = 'account' @@ -41,11 +56,11 @@ const accountPlugin = plugin(accountId, { Secret: '' as Metadata }, status: { - AccountNotFound: '' as StatusCode<{account: string}>, - WorkspaceNotFound: '' as StatusCode<{workspace: string}>, - InvalidPassword: '' as StatusCode<{account: string}>, - AccountAlreadyExists: '' as StatusCode<{account: string}>, - WorkspaceAlreadyExists: '' as StatusCode<{workspace: string}> + AccountNotFound: '' as StatusCode<{ account: string }>, + WorkspaceNotFound: '' as StatusCode<{ workspace: string }>, + InvalidPassword: '' as StatusCode<{ account: string }>, + AccountAlreadyExists: '' as StatusCode<{ account: string }>, + WorkspaceAlreadyExists: '' as StatusCode<{ workspace: string }> } }) @@ -150,8 +165,25 @@ function generateToken (email: string, workspace: string): string { * @param workspace - * @returns */ -export async function login (db: Db, email: string, password: string, workspace: string): Promise { - const accountInfo = await getAccountInfo(db, email, password) +export async function login (db: Db, email: string, password: string): Promise { + await getAccountInfo(db, email, password) + const result = { + endpoint: getEndpoint(), + email, + token: generateToken(email, '') + } + return result +} + +/** + * @public + */ +export async function selectWorkspace (db: Db, token: string, workspace: string): Promise { + const { email } = decode(token, getSecret()) + const accountInfo = await getAccount(db, email) + if (accountInfo === null) { + throw new PlatformError(new Status(Severity.ERROR, accountPlugin.status.AccountNotFound, { account: email })) + } const workspaceInfo = await getWorkspace(db, workspace) if (workspaceInfo !== null) { @@ -175,7 +207,43 @@ export async function login (db: Db, email: string, password: string, workspace: /** * @public */ -export async function createAccount (db: Db, email: string, password: string, first: string, last: string): Promise { +export async function join (db: Db, email: string, password: string, workspaceHash: string): Promise { + const workspace = decode(workspaceHash, getSecret()) + const token = (await login(db, email, password)).token + await assignWorkspace(db, email, workspace) + + return await selectWorkspace(db, token, workspace) +} + +/** + * @public + */ +export async function signUpJoin ( + db: Db, + email: string, + password: string, + first: string, + last: string, + workspaceHash: string +): Promise { + await createAccount(db, email, password, first, last) + const workspace = decode(workspaceHash, getSecret()) + await assignWorkspace(db, email, workspace) + + const token = (await login(db, email, password)).token + return await selectWorkspace(db, token, workspace) +} + +/** + * @public + */ +export async function createAccount ( + db: Db, + email: string, + password: string, + first: string, + last: string +): Promise { const salt = randomBytes(32) const hash = hashWithSalt(password, salt) @@ -184,7 +252,7 @@ export async function createAccount (db: Db, email: string, password: string, fi throw new PlatformError(new Status(Severity.ERROR, accountPlugin.status.AccountAlreadyExists, { account: email })) } - const insert = await db.collection(ACCOUNT_COLLECTION).insertOne({ + await db.collection(ACCOUNT_COLLECTION).insertOne({ email, hash, salt, @@ -193,31 +261,26 @@ export async function createAccount (db: Db, email: string, password: string, fi workspaces: [] }) - return { - _id: insert.insertedId, - first, - last, + const result = { + endpoint: getEndpoint(), email, - workspaces: [] + token: generateToken(email, '') } + return result } /** * @public */ export async function listWorkspaces (db: Db): Promise { - return await db.collection(WORKSPACE_COLLECTION) - .find({}) - .toArray() + return await db.collection(WORKSPACE_COLLECTION).find({}).toArray() } /** * @public */ export async function listAccounts (db: Db): Promise { - return await db.collection(ACCOUNT_COLLECTION) - .find({}) - .toArray() + return await db.collection(ACCOUNT_COLLECTION).find({}).toArray() } /** @@ -227,16 +290,64 @@ export async function createWorkspace (db: Db, workspace: string, organisation: if ((await getWorkspace(db, workspace)) !== null) { throw new PlatformError(new Status(Severity.ERROR, accountPlugin.status.WorkspaceAlreadyExists, { workspace })) } - return await db + const result = await db .collection(WORKSPACE_COLLECTION) .insertOne({ workspace, organisation }) .then((e) => e.insertedId.toHexString()) + await initWorkspace(getEndpoint(), workspace) + return result } -async function getWorkspaceAndAccount (db: Db, email: string, workspace: string): Promise<{ accountId: ObjectId, workspaceId: ObjectId }> { +/** + * @public + */ +export async function createUserWorkspace (db: Db, token: string, workspace: string): Promise { + const { email } = decode(token, getSecret()) + await createWorkspace(db, workspace, '') + await assignWorkspace(db, email, workspace) + const result = { + endpoint: getEndpoint(), + email, + token: generateToken(email, workspace) + } + return result +} + +/** + * @public + */ +export async function getWorkspaceHash (db: Db, token: string): Promise { + const { workspace } = decode(token, getSecret()) + const wsPromise = await getWorkspace(db, workspace) + if (wsPromise === null) { + throw new PlatformError(new Status(Severity.ERROR, accountPlugin.status.WorkspaceNotFound, { workspace })) + } + return encode(workspace, getSecret()) +} + +/** + * @public + */ +export async function getUserWorkspaces (db: Db, token: string): Promise { + const { email } = decode(token, getSecret()) + const account = await getAccount(db, email) + if (account === null) return [] + return await db + .collection(WORKSPACE_COLLECTION) + .find({ + _id: { $in: account.workspaces } + }) + .toArray() +} + +async function getWorkspaceAndAccount ( + db: Db, + email: string, + workspace: string +): Promise<{ accountId: ObjectId, workspaceId: ObjectId }> { const wsPromise = await getWorkspace(db, workspace) if (wsPromise === null) { throw new PlatformError(new Status(Severity.ERROR, accountPlugin.status.WorkspaceNotFound, { workspace })) @@ -260,6 +371,30 @@ export async function assignWorkspace (db: Db, email: string, workspace: string) // Add workspace to account await db.collection(ACCOUNT_COLLECTION).updateOne({ _id: accountId }, { $push: { workspaces: workspaceId } }) + + const account = await db.collection(ACCOUNT_COLLECTION).findOne({ _id: accountId }) + + if (account !== null) await createEmployeeAccount(account, workspace) +} + +async function createEmployeeAccount (account: Account, workspace: string): Promise { + const connection = await connect(getEndpoint(), workspace, account.email) + const ops = new TxOperations(connection, core.account.System) + + const name = combineName(account.first, account.last) + + const employee = await ops.createDoc(contact.class.Employee, contact.space.Employee, { + name, + city: '', + channels: [] + }) + + await ops.createDoc(contact.class.EmployeeAccount, core.space.Model, { + email: account.email, + employee, + name + }) + await connection.close() } /** @@ -284,7 +419,9 @@ export async function dropWorkspace (db: Db, workspace: string): Promise { throw new PlatformError(new Status(Severity.ERROR, accountPlugin.status.WorkspaceNotFound, { workspace })) } await db.collection(WORKSPACE_COLLECTION).deleteOne({ _id: ws._id }) - await db.collection(ACCOUNT_COLLECTION).updateMany({ _id: { $in: ws.accounts } }, { $pull: { workspaces: ws._id } }) + await db + .collection(ACCOUNT_COLLECTION) + .updateMany({ _id: { $in: ws.accounts } }, { $pull: { workspaces: ws._id } }) } /** @@ -296,14 +433,22 @@ export async function dropAccount (db: Db, email: string): Promise { throw new PlatformError(new Status(Severity.ERROR, accountPlugin.status.AccountNotFound, { account: email })) } await db.collection(ACCOUNT_COLLECTION).deleteOne({ _id: account._id }) - await db.collection(WORKSPACE_COLLECTION).updateMany({ _id: { $in: account.workspaces } }, { $pull: { accounts: account._id } }) + await db + .collection(WORKSPACE_COLLECTION) + .updateMany({ _id: { $in: account.workspaces } }, { $pull: { accounts: account._id } }) } function wrap (f: (db: Db, ...args: any[]) => Promise) { - return async function (db: Db, request: Request): Promise> { + return async function (db: Db, request: Request, token?: string): Promise> { + if (token !== undefined) request.params.unshift(token) return await f(db, ...request.params) .then((result) => ({ id: request.id, result })) - .catch((err) => ({ error: err instanceof PlatformError ? new Status(Severity.ERROR, platform.status.Forbidden, {}) : new Status(Severity.ERROR, platform.status.InternalServerError, {}) })) + .catch((err) => ({ + error: + err instanceof PlatformError + ? new Status(Severity.ERROR, platform.status.Forbidden, {}) + : new Status(Severity.ERROR, platform.status.InternalServerError, {}) + })) } } @@ -312,9 +457,14 @@ function wrap (f: (db: Db, ...args: any[]) => Promise) { */ export const methods = { login: wrap(login), + join: wrap(join), + signUpJoin: wrap(signUpJoin), + selectWorkspace: wrap(selectWorkspace), + getUserWorkspaces: wrap(getUserWorkspaces), + getWorkspaceHash: wrap(getWorkspaceHash), getAccountInfo: wrap(getAccountInfo), createAccount: wrap(createAccount), - createWorkspace: wrap(createWorkspace), + createWorkspace: wrap(createUserWorkspace), assignWorkspace: wrap(assignWorkspace), removeWorkspace: wrap(removeWorkspace), listWorkspaces: wrap(listWorkspaces) diff --git a/server/account/src/tool.ts b/server/account/src/tool.ts new file mode 100644 index 0000000000..2eda019222 --- /dev/null +++ b/server/account/src/tool.ts @@ -0,0 +1,87 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// Copyright © 2021, 2022 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import core, { DOMAIN_TX, Tx } from '@anticrm/core' +import builder, { createDeps } from '@anticrm/model-all' +import { Client } from 'minio' +import { Document, MongoClient } from 'mongodb' +import { connect } from './connect' + +export async function initWorkspace (transactorUrl: string, dbName: string): Promise { + const minioEndpoint = process.env.MINIO_ENDPOINT + if (minioEndpoint === undefined) { + console.error('please provide minio endpoint') + process.exit(1) + } + + const minioAccessKey = process.env.MINIO_ACCESS_KEY + if (minioAccessKey === undefined) { + console.error('please provide minio access key') + process.exit(1) + } + + const minioSecretKey = process.env.MINIO_SECRET_KEY + if (minioSecretKey === undefined) { + console.error('please provide minio secret key') + process.exit(1) + } + + const mongodbUri = process.env.MONGO_URL + if (mongodbUri === undefined) { + console.error('please provide mongodb url.') + process.exit(1) + } + + const minio = new Client({ + endPoint: minioEndpoint, + port: 9000, + useSSL: false, + accessKey: minioAccessKey, + secretKey: minioSecretKey + }) + + const txes = JSON.parse(JSON.stringify(builder.getTxes())) as Tx[] + + if (txes.some((tx) => tx.objectSpace !== core.space.Model)) { + throw Error('Model txes must target only core.space.Model') + } + + const client = new MongoClient(mongodbUri) + try { + await client.connect() + const db = client.db(dbName) + + console.log('dropping database...') + await db.dropDatabase() + + console.log('creating model...') + const model = txes + const result = await db.collection(DOMAIN_TX).insertMany(model as Document[]) + console.log(`${result.insertedCount} model transactions inserted.`) + + console.log('creating data...') + const connection = await connect(transactorUrl, dbName) + await createDeps(connection) + await connection.close() + + console.log('create minio bucket') + if (!(await minio.bucketExists(dbName))) { + await minio.makeBucket(dbName, 'k8s') + } + } finally { + await client.close() + } +}