Signed-off-by: Denis Bykhov <80476319+BykhovDenis@users.noreply.github.com>
This commit is contained in:
Denis Bykhov 2022-01-11 15:23:17 +06:00 committed by GitHub
parent a973d2a0fd
commit 459d7b3261
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 1259 additions and 423 deletions

View File

@ -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

View File

@ -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:

View File

@ -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)
})
})

View File

@ -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<void> {
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<void> {
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')
}

View File

@ -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
</script>
<div class="editbox{error ? ' error' : ''}" style={width ? 'width: ' + width : ''}>
@ -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

View File

@ -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<CompAndProps[]>([])
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<LabelAndProps>({
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)
})

View File

@ -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"
}
}

View File

@ -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"
}

View File

@ -0,0 +1,68 @@
<!--
// 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.
-->
<script lang="ts">
import { Status, Severity } from '@anticrm/platform'
import Form from './Form.svelte'
import { createWorkspace } from '../utils'
import { getCurrentLocation, navigate, setMetadataLocalStorage, showPopup } from '@anticrm/ui'
import login from '../plugin'
import workbench from '@anticrm/workbench'
import InviteLink from './InviteLink.svelte'
const fields = [{ name: 'workspace', i18n: login.string.Workspace, rule: /^\S+$/ }]
const object = {
workspace: ''
}
let status = new Status(Severity.OK, 0, '')
const action = {
i18n: login.string.CreateWorkspace,
func: async () => {
status = new Status(Severity.INFO, login.status.ConnectingToServer, {})
const [loginStatus, result] = await createWorkspace(object.workspace)
status = loginStatus
if (result !== undefined) {
setMetadataLocalStorage(login.metadata.LoginToken, result.token)
setMetadataLocalStorage(login.metadata.LoginEndpoint, result.endpoint)
setMetadataLocalStorage(login.metadata.LoginEmail, result.email)
showPopup(InviteLink, {}, undefined, () => {
navigate({ path: [workbench.component.WorkbenchApp] })
})
}
}
}
</script>
<Form
caption={login.string.CreateWorkspace}
{status}
{fields}
{object}
{action}
bottomCaption={login.string.HaveWorkspace}
bottomActionLabel={login.string.SelectWorkspace}
bottomActionFunc={() => {
const loc = getCurrentLocation()
loc.path[1] = 'selectWorkspace'
loc.path.length = 2
navigate(loc)
}}
/>

View File

@ -1,5 +1,6 @@
<!--
// 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
@ -22,11 +23,13 @@
import login from '../plugin'
interface Field {
id?: string
name: string
i18n: IntlString
password?: boolean
optional?: boolean
short?: boolean
rule?: RegExp
}
interface Action {
@ -51,6 +54,25 @@
status = new Status(Severity.INFO, login.status.RequiredField, { field: await translate(field.i18n, {}) })
return
}
if (f.id !== undefined) {
const sameFields = fields.filter((f) => f.id === field.id)
for (const field of sameFields) {
const v = object[field.name]
if (v !== object[f.name]) {
status = new Status(Severity.INFO, login.status.FieldsDoNotMatch, {
field: await translate(field.i18n, {}),
field2: await translate(f.i18n, {})
})
return
}
}
}
if (f.rule !== undefined) {
if (!f.rule.test(v)) {
status = new Status(Severity.INFO, login.status.IncorrectValue, { field: await translate(field.i18n, {}) })
return
}
}
}
status = OK
}
@ -84,6 +106,7 @@
<div class={field.short ? 'form-col' : 'form-row'}>
<StylishEdit
label={field.i18n}
name={field.id}
password={field.password}
bind:value={object[field.name]}
on:keyup={validate}
@ -107,12 +130,6 @@
}}
/>
</div>
<!-- <div class="form-col"><EditBox label="First Name" bind:value={fname}/></div>
<div class="form-col"><EditBox label="Last Name" bind:value={lname}/></div>
<div class="form-row"><EditBox label="E-mail"/></div>
<div class="form-row"><EditBox label="Password" password/></div>
<div class="form-row"><EditBox label="Repeat password" password/></div> -->
</div>
<div class="grow-separator" />
<div class="footer">
@ -121,11 +138,6 @@
</div>
</form>
<!-- <div class="actions">
{#each actions as action, i}
<button class="button" class:separator={i !== 0} on:click|preventDefault={action.func}> {action.i18n} </button>
{/each}
</div> -->
<style lang="scss">
.container {
display: flex;

View File

@ -0,0 +1,89 @@
<!--
// 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.
-->
<script lang="ts">
import { Button, getCurrentLocation, Label, locationToUrl } from '@anticrm/ui'
import { getWorkspaceHash } from '../utils'
import { createEventDispatcher } from 'svelte'
import login from '../plugin'
const dispatch = createEventDispatcher()
async function getLink (): Promise<string> {
const hash = await getWorkspaceHash()
const loc = getCurrentLocation()
loc.path[0] = login.component.LoginApp
loc.path[1] = 'join'
loc.path.length = 2
loc.query = {
workspace: hash
}
const link = locationToUrl(loc)
return document.location.origin + link
}
function copy (link: string): void {
navigator.clipboard.writeText(link)
}
</script>
<div class="popup">
<div class="fs-title flex-center">
<Label label={login.string.InviteDescription} />
</div>
{#await getLink() then link}
<div class="link">{link}</div>
<div class="buttons flex">
<Button
label={login.string.Copy}
size="small"
on:click={() => {
copy(link)
}}
/>
<Button
label={login.string.Close}
size="small"
primary
on:click={() => {
dispatch('close')
}}
/>
</div>
{/await}
</div>
<style lang="scss">
.popup {
display: flex;
flex-direction: column;
padding: 1rem;
color: var(--theme-caption-color);
background-color: var(--theme-button-bg-hovered);
border: 1px solid var(--theme-button-border-enabled);
border-radius: 0.75rem;
filter: drop-shadow(0 1.5rem 4rem rgba(0, 0, 0, 0.35));
.link {
margin-top: 2rem;
margin-bottom: 2rem;
}
.buttons {
justify-content: space-around;
align-items: center;
}
}
</style>

View File

@ -0,0 +1,100 @@
<!--
// 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.
-->
<script lang="ts">
import { OK, Status, Severity } from '@anticrm/platform'
import { getCurrentLocation, navigate, setMetadataLocalStorage } from '@anticrm/ui'
import Form from './Form.svelte'
import { join, signUpJoin } from '../utils'
import login from '../plugin'
import workbench from '@anticrm/workbench'
const location = getCurrentLocation()
let page = 'login'
$: fields =
page === 'login'
? [
{ id: 'email', name: 'username', i18n: login.string.Email },
{
id: 'current-password',
name: 'password',
i18n: login.string.Password,
password: true
}
]
: [
{ id: 'given-name', name: 'first', i18n: login.string.FirstName, short: true },
{ id: 'family-name', name: 'last', i18n: login.string.LastName, short: true },
{ id: 'email', name: 'username', i18n: login.string.Email },
{ id: 'new-password', name: 'password', i18n: login.string.Password, password: true },
{ id: 'new-password', name: 'password2', i18n: login.string.PasswordRepeat, password: true }
]
$: object = {
first: '',
last: '',
username: '',
password: '',
password2: ''
}
let status = OK
$: action = {
i18n: login.string.Join,
func: async () => {
status = new Status(Severity.INFO, login.status.ConnectingToServer, {})
const [loginStatus, result] =
page === 'login'
? await join(object.username, object.password, location.query?.workspace ?? '')
: await signUpJoin(
object.username,
object.password,
object.first,
object.last,
location.query?.workspace ?? ''
)
status = loginStatus
if (result !== undefined) {
setMetadataLocalStorage(login.metadata.LoginToken, result.token)
setMetadataLocalStorage(login.metadata.LoginEndpoint, result.endpoint)
setMetadataLocalStorage(login.metadata.LoginEmail, result.email)
navigate({ path: [workbench.component.WorkbenchApp] })
}
}
}
$: bottomCaption = page === 'login' ? login.string.DoNotHaveAnAccount : login.string.DoNotHaveAnAccount
$: bottomActionLabel = page === 'login' ? login.string.SignUp : login.string.LogIn
</script>
<Form
caption={login.string.Join}
{status}
{fields}
{object}
{action}
{bottomCaption}
{bottomActionLabel}
bottomActionFunc={() => {
page = page === 'login' ? 'signUp' : 'login'
}}
/>

View File

@ -1,42 +1,64 @@
<!--
// Copyright © 2020 Anticrm Platform Contributors.
//
// 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.
-->
<script lang="ts">
import { fetchMetadataLocalStorage, location, Popup } from '@anticrm/ui'
import LoginForm from './LoginForm.svelte'
import SignupForm from './SignupForm.svelte'
import CreateWorkspaceForm from './CreateWorkspaceForm.svelte'
import SelectWorkspace from './SelectWorkspace.svelte'
import Join from './Join.svelte'
import { onDestroy } from 'svelte'
import login from '../plugin'
let page = 'login'
export let page: string = 'login'
const token = fetchMetadataLocalStorage(login.metadata.LoginToken)
onDestroy(
location.subscribe(async (loc) => {
page = loc.path[1] ?? (token ? 'selectWorkspace' : 'login')
})
)
</script>
<div class="container">
<div class="panel">
{#if page === 'login'}
<LoginForm on:switch={(event) => page = event.detail}/>
<LoginForm />
{:else if page === 'signup'}
<SignupForm on:switch={(event) => page = event.detail}/>
<SignupForm />
{:else if page === 'createWorkspace'}
<CreateWorkspaceForm />
{:else if page === 'selectWorkspace'}
<SelectWorkspace />
{:else if page === 'join'}
<Join />
{/if}
</div>
<div class="intro">
<div class="content">
<div class="logo"> </div>
<div class="logo" />
</div>
<div class="slogan">
<p>A unique place to manage all of your work</p>
<p>Welcome to the Platform</p>
</div>
</div>
<Popup />
</div>
<style lang="scss">
@ -71,21 +93,21 @@
position: relative;
&:after {
position: absolute;
content: "";
background: center url("../../img/logo.svg");
content: '';
background: center url('../../img/logo.svg');
transform: translate(-50%, -50%);
width: 63px;
height: 79px;
}
&:before {
position: absolute;
content: "";
content: '';
transform: translate(-50%, -50%);
width: 16rem;
height: 16rem;
border: 1.8px solid var(--theme-caption-color);
border-radius: 50%;
opacity: .08;
opacity: 0.08;
}
}
}
@ -94,12 +116,12 @@
p {
margin: 0;
font-weight: 400;
font-size: .8rem;
font-size: 0.8rem;
text-align: center;
color: var(--theme-caption-color);
opacity: .8;
opacity: 0.8;
}
}
}
}
</style>
</style>

View File

@ -1,72 +1,77 @@
<!--
// Copyright © 2020 Anticrm Platform Contributors.
//
// 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.
-->
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import { OK, Status, Severity } from '@anticrm/platform'
import { navigate, setMetadataLocalStorage } from '@anticrm/ui'
import { getCurrentLocation, navigate, setMetadataLocalStorage } from '@anticrm/ui'
import Form from './Form.svelte'
import { doLogin } from '../utils'
import login from '../plugin'
import workbench from '@anticrm/workbench'
const dispatch = createEventDispatcher()
const fields = [
{ name: 'username', i18n: login.string.Email },
{ id: 'email', name: 'username', i18n: login.string.Email },
{
id: 'current-password',
name: 'password',
i18n: login.string.Password,
password: true
},
{ name: 'workspace', i18n: login.string.Workspace }
}
]
const object = {
workspace: '',
username: '',
password: '',
password: ''
}
let status = OK
const action = {
const action = {
i18n: login.string.LogIn,
func: async () => {
func: async () => {
status = new Status(Severity.INFO, login.status.ConnectingToServer, {})
const [loginStatus, result] = await doLogin(object.username, object.password, object.workspace)
const [loginStatus, result] = await doLogin(object.username, object.password)
status = loginStatus
if (result !== undefined) {
console.log('token', result.token)
setMetadataLocalStorage(login.metadata.LoginToken, result.token)
setMetadataLocalStorage(login.metadata.LoginEndpoint, result.endpoint)
setMetadataLocalStorage(login.metadata.LoginEmail, result.email)
navigate({ path: [workbench.component.WorkbenchApp] })
const loc = getCurrentLocation()
loc.path[1] = 'selectWorkspace'
loc.path.length = 2
navigate(loc)
}
}
}
</script>
<Form caption={login.string.LogIn} {status} {fields} {object} {action}
<Form
caption={login.string.LogIn}
{status}
{fields}
{object}
{action}
bottomCaption={login.string.DoNotHaveAnAccount}
bottomActionLabel={login.string.SignUp}
bottomActionFunc={() => { dispatch('switch', 'signup') }}
bottomActionFunc={() => {
const loc = getCurrentLocation()
loc.path[1] = 'signup'
loc.path.length = 2
navigate(loc)
}}
/>

View File

@ -1,146 +0,0 @@
<!--
// Copyright © 2020, 2021 Anticrm Platform Contributors.
//
// 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.
-->
<script lang="ts">
import type { Platform } from '@anticrm/plugin'
import { AuthStatusCodes } from '@anticrm/plugin'
import { Severity, Status } from '@anticrm/status'
import { getContext, onDestroy } from 'svelte'
import type { LoginService } from '@anticrm/plugin-login'
import login from '@anticrm/plugin-login'
import Form from './Form.svelte'
import { Button } from '@anticrm/ui'
import type { ApplicationRouter } from '@anticrm/plugin-ui'
// import { PlatformStatusCodes } from '@anticrm/foundation'
// export let router: ApplicationRouter<ApplicationRoute>
const object = { username: '', password: '', workspace: '', secondFactorCode: '' }
let status: Status
let loginActive = false
let needSecondFactor = false
const baseFields = [
{ name: 'username', i18n: 'Username' },
{
name: 'password',
i18n: 'Password',
password: true
},
{ name: 'workspace', i18n: 'Workspace' }
]
let fields: { [key: string]: any }
$: fields = needSecondFactor ? baseFields.concat({ name: 'secondFactorCode', i18n: 'Confirm code' }) : baseFields
const platform = getContext('platform') as Platform
const loginService = platform.getPlugin(login.id)
async function doLogin () {
status = new Status(Severity.INFO, 0, 'Соединяюсь с сервером...')
status = await (await loginService).doLogin(
object.username,
object.password,
object.workspace,
object.secondFactorCode
)
if (status.code === AuthStatusCodes.CLIENT_VALIDATE_REQUIRED) {
needSecondFactor = true
}
}
// eslint-disable-next-line @typescript-eslint/no-empty-function
async function doSignup () {}
async function checkLoginInfo (ls: LoginService) {
const info = await ls.getLoginInfo()
if (info) {
loginActive = true
object.username = info.email
object.workspace = info.workspace
}
}
let timer: number
// Auto forward to default application
const loginCheck = loginService.then(async (ls) => {
await checkLoginInfo(ls)
timer = setInterval(async () => {
await checkLoginInfo(ls)
}, 1000)
})
onDestroy(() => {
if (timer) {
clearInterval(timer)
}
})
async function navigateApp (): Promise<void> {
(await loginService).navigateApp()
}
async function logout (): Promise<void> {
(await loginService).doLogout()
loginActive = false
}
function navigateSetting (): void {
// router.navigate({ route: 'setting' })
}
</script>
{#await loginCheck then value}
{#if loginActive}
<div class="login-form-info">
<div class="field">
Logged in as: {object.username}
</div>
<div class="field">
Workspace: {object.workspace}
</div>
<div class="actions">
<Button width="100px" on:click={logout}>Logout</Button>
<Button width="100px" on:click={navigateSetting}>Settings</Button>
<Button width="100px" on:click={navigateApp}>Switch to Application</Button>
</div>
</div>
{:else}
<Form
actions={[
{ i18n: 'Create Space', func: doSignup },
{ i18n: 'Login', func: doLogin }
]}
{fields}
{object}
caption="Login into system"
{status} />
{/if}
{/await}
<style lang="scss">
.login-form-info {
margin: 20vh auto auto;
width: 30em;
padding: 2em;
border-radius: 1em;
border: 1px solid var(--theme-bg-accent-color);
.actions {
display: flex;
margin-top: 1.5em;
}
}
</style>

View File

@ -1,45 +0,0 @@
<!--
// Copyright © 2020, 2021 Anticrm Platform Contributors.
//
// 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.
-->
<script lang="ts">
import login from '..'
import type { AnyComponent, ApplicationRoute } from '@anticrm/platform-ui'
import { newRouter } from '@anticrm/platform-ui'
import Component from '@anticrm/platform-ui/src/components/Component.svelte'
let form: ApplicationRoute
const forms: ApplicationRoute[] = [{ route: 'setting', component: login.component.SettingForm }]
let component: AnyComponent | undefined
function routeDefaults (): ApplicationRoute {
return {
route: '#undefined',
component: login.component.LoginForm
} as ApplicationRoute
}
const router = newRouter<ApplicationRoute>(
':route',
(info) => {
if (forms.length > 0) {
form = forms.find((a) => a.route === info.route) || routeDefaults()
component = form?.component
}
},
routeDefaults()
)
</script>
<Component is={component} props={{ router }} />

View File

@ -0,0 +1,138 @@
<!--
// 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.
-->
<script lang="ts">
import { Status, Severity, OK } from '@anticrm/platform'
import { getWorkspaces, selectWorkspace } from '../utils'
import { Button, getCurrentLocation, Label, navigate, setMetadataLocalStorage } from '@anticrm/ui'
import workbench from '@anticrm/workbench'
import login from '../plugin'
import StatusControl from './StatusControl.svelte'
let status = OK
async function select (workspace: string) {
status = new Status(Severity.INFO, login.status.ConnectingToServer, {})
const [loginStatus, result] = await selectWorkspace(workspace)
status = loginStatus
if (result !== undefined) {
setMetadataLocalStorage(login.metadata.LoginToken, result.token)
setMetadataLocalStorage(login.metadata.LoginEndpoint, result.endpoint)
setMetadataLocalStorage(login.metadata.LoginEmail, result.email)
navigate({ path: [workbench.component.WorkbenchApp] })
}
}
function createWorkspace (): void {
const loc = getCurrentLocation()
loc.path[1] = 'createWorkspace'
loc.path.length = 2
navigate(loc)
}
</script>
<form class="container">
<div class="grow-separator" />
<div class="title"><Label label={login.string.SelectWorkspace} /></div>
<div class="status">
<StatusControl {status} />
</div>
{#await getWorkspaces() then workspaces}
<div class="form">
{#each workspaces as workspace (workspace._id)}
<div
class="workspace flex-center fs-title cursor-pointer focused-button form-row"
on:click={() => select(workspace.workspace)}
>
{workspace.workspace}
</div>
{/each}
{#if !workspaces.length}
<div class="form-row send">
<Button label={login.string.CreateWorkspace} primary width="100%" on:click={createWorkspace} />
</div>
{/if}
</div>
<div class="grow-separator" />
{#if workspaces.length}
<div class="footer">
<span><Label label={login.string.CreateWorkspace} /></span>
<a href="." on:click|preventDefault={createWorkspace}><Label label={login.string.CreateWorkspace} /></a>
</div>
{/if}
{/await}
</form>
<style lang="scss">
.container {
display: flex;
flex-direction: column;
justify-content: space-between;
overflow: hidden;
height: 100%;
padding: 5rem;
.title {
font-weight: 600;
font-size: 1.5rem;
color: var(--theme-caption-color);
}
.status {
min-height: 7.5rem;
max-height: 7.5rem;
padding-top: 1.25rem;
}
.form {
display: grid;
grid-template-columns: 1fr 1fr;
column-gap: 0.75rem;
row-gap: 1.5rem;
.form-row {
grid-column-start: 1;
grid-column-end: 3;
}
.workspace {
padding: 1rem;
border-radius: 1rem;
}
}
.grow-separator {
flex-grow: 1;
}
.footer {
margin-top: 3.5rem;
font-size: 0.8rem;
color: var(--theme-caption-color);
span {
opacity: 0.3;
}
a {
text-decoration: none;
color: var(--theme-caption-color);
opacity: 0.8;
&:hover {
opacity: 1;
}
}
}
}
</style>

View File

@ -1,68 +1,78 @@
<!--
// Copyright © 2020 Anticrm Platform Contributors.
//
// 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.
-->
<script lang="ts">
import { getContext, createEventDispatcher } from 'svelte'
import { Status, Severity } from '@anticrm/platform'
import Form from './Form.svelte'
import { doLogin } from '../utils'
const dispatch = createEventDispatcher()
import { signUp } from '../utils'
import login from '../plugin'
import { getCurrentLocation, navigate, setMetadataLocalStorage } from '@anticrm/ui'
const fields = [
{ name: 'first', i18n: 'First name', short: true },
{ name: 'last', i18n: 'Last name', short: true },
{ name: 'username', i18n: 'Email' },
{ name: 'workspace', i18n: 'Workspace' },
{ name: 'password', i18n: 'Password', password: true },
{ name: 'password2', i18n: 'Repeat password', password: true },
{ id: 'given-name', name: 'first', i18n: login.string.FirstName, short: true },
{ id: 'family-name', name: 'last', i18n: login.string.LastName, short: true },
{ id: 'email', name: 'username', i18n: login.string.Email },
{ id: 'new-password', name: 'password', i18n: login.string.Password, password: true },
{ id: 'new-password', name: 'password2', i18n: login.string.PasswordRepeat, password: true }
]
const object = {
first: '',
last: '',
workspace: '',
username: '',
password: '',
password2: '',
password2: ''
}
let status = new Status(Severity.OK, 0, '')
const action = {
i18n: 'Sign Up',
func: async () => {
status = new Status(Severity.INFO, 0, 'Соединяюсь с сервером...')
const action = {
i18n: login.string.SignUp,
func: async () => {
status = new Status(Severity.INFO, login.status.ConnectingToServer, {})
const [loginStatus, result] = await doLogin(object.username, object.password, object.workspace)
const [loginStatus, result] = await signUp(object.username, object.password, object.first, object.last)
return new Promise<void>((resolve, reject) => {
setTimeout(() => {
status = loginStatus
resolve()
}, 1000)
})
status = loginStatus
if (result !== undefined) {
setMetadataLocalStorage(login.metadata.LoginToken, result.token)
setMetadataLocalStorage(login.metadata.LoginEndpoint, result.endpoint)
setMetadataLocalStorage(login.metadata.LoginEmail, result.email)
const loc = getCurrentLocation()
loc.path[1] = 'selectWorkspace'
loc.path.length = 2
navigate(loc)
}
}
}
</script>
<Form caption="Sign Up" {status} {fields} {object} {action}
bottomCaption="Already have an account?"
bottomActionLabel="Log In"
bottomActionFunc={() => { dispatch('switch', 'login') }}
<Form
caption={login.string.SignUp}
{status}
{fields}
{object}
{action}
bottomCaption={login.string.HaveAccount}
bottomActionLabel={login.string.LogIn}
bottomActionFunc={() => {
const loc = getCurrentLocation()
loc.path[1] = 'login'
loc.path.length = 2
navigate(loc)
}}
/>

View File

@ -12,7 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import type { Status } from '@anticrm/platform'
import { Severity } from '@anticrm/platform'
@ -23,17 +22,17 @@
</script>
{#if status.severity !== Severity.OK}
<div class="flex-row-center container" class:error={status.severity === Severity.ERROR}>
<StatusControl {status} />
</div>
<div class="flex-row-center container" class:error={status.severity === Severity.ERROR}>
<StatusControl {status} />
</div>
{/if}
<style lang="scss">
.container {
padding: .75rem 1rem;
padding: 0.75rem 1rem;
background-color: var(--theme-bg-accent-color);
border: 1px solid var(--theme-bg-accent-hover);
border-radius: .5rem;
border-radius: 0.5rem;
}
.error {
@ -42,4 +41,4 @@
background-color: var(--theme-button-bg-error);
border-color: var(--system-error-60-color);
}
</style>
</style>

View File

@ -26,5 +26,5 @@ import LoginApp from './components/LoginApp.svelte'
export default async () => ({
component: {
LoginApp
},
}
})

View File

@ -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
}
})

View File

@ -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<any> = 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<any> = await response.json()
return [result.error ?? OK, result.result]
} catch (err) {
return [unknownError(err), undefined]
}
}
export async function getWorkspaces (): Promise<Workspace[]> {
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<any> = 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<any> = await response.json()
return [result.error ?? OK, result.result]
} catch (err) {
return [unknownError(err), undefined]
}
}
export async function getWorkspaceHash (): Promise<string> {
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<any> = 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<any> = 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<any> = await response.json()
return [result.error ?? OK, result.result]
} catch (err) {
return [unknownError(err), undefined]
}
}

View File

@ -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

View File

@ -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"
},

View File

@ -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<any>) => Response<any> })[request.method]
const method = (methods as { [key: string]: (db: Db, request: Request<any>, token?: string) => Response<any> })[
request.method
]
if (method === undefined) {
const response: Response<void> = {
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
})

View File

@ -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"
}
}

View File

@ -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<Client> {
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)
}

View File

@ -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<string>
},
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<LoginInfo> {
const accountInfo = await getAccountInfo(db, email, password)
export async function login (db: Db, email: string, password: string): Promise<LoginInfo> {
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<LoginInfo> {
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<AccountInfo> {
export async function join (db: Db, email: string, password: string, workspaceHash: string): Promise<LoginInfo> {
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<LoginInfo> {
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<LoginInfo> {
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<Workspace[]> {
return await db.collection<Workspace>(WORKSPACE_COLLECTION)
.find({})
.toArray()
return await db.collection<Workspace>(WORKSPACE_COLLECTION).find({}).toArray()
}
/**
* @public
*/
export async function listAccounts (db: Db): Promise<Account[]> {
return await db.collection<Account>(ACCOUNT_COLLECTION)
.find({})
.toArray()
return await db.collection<Account>(ACCOUNT_COLLECTION).find({}).toArray()
}
/**
@ -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<LoginInfo> {
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<string> {
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<Workspace[]> {
const { email } = decode(token, getSecret())
const account = await getAccount(db, email)
if (account === null) return []
return await db
.collection<Workspace>(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>(ACCOUNT_COLLECTION).findOne({ _id: accountId })
if (account !== null) await createEmployeeAccount(account, workspace)
}
async function createEmployeeAccount (account: Account, workspace: string): Promise<void> {
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<void> {
throw new PlatformError(new Status(Severity.ERROR, accountPlugin.status.WorkspaceNotFound, { workspace }))
}
await db.collection(WORKSPACE_COLLECTION).deleteOne({ _id: ws._id })
await db.collection<Account>(ACCOUNT_COLLECTION).updateMany({ _id: { $in: ws.accounts } }, { $pull: { workspaces: ws._id } })
await db
.collection<Account>(ACCOUNT_COLLECTION)
.updateMany({ _id: { $in: ws.accounts } }, { $pull: { workspaces: ws._id } })
}
/**
@ -296,14 +433,22 @@ export async function dropAccount (db: Db, email: string): Promise<void> {
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>(WORKSPACE_COLLECTION).updateMany({ _id: { $in: account.workspaces } }, { $pull: { accounts: account._id } })
await db
.collection<Workspace>(WORKSPACE_COLLECTION)
.updateMany({ _id: { $in: account.workspaces } }, { $pull: { accounts: account._id } })
}
function wrap (f: (db: Db, ...args: any[]) => Promise<any>) {
return async function (db: Db, request: Request<any[]>): Promise<Response<any>> {
return async function (db: Db, request: Request<any[]>, token?: string): Promise<Response<any>> {
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<any>) {
*/
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)

View File

@ -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<void> {
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()
}
}