From 6011a75e05035f37050b1fffde1c2b36a01dc9a1 Mon Sep 17 00:00:00 2001
From: Andrey Platov <andrey@hardcoreeng.com>
Date: Thu, 9 Sep 2021 20:42:34 +0200
Subject: [PATCH] initial `account` contribution

Signed-off-by: Andrey Platov <andrey@hardcoreeng.com>
---
 common/config/rush/pnpm-lock.yaml |  30 ++++++-
 rush.json                         |   5 ++
 server/account/.eslintrc.js       |   6 ++
 server/account/.npmignore         |   4 +
 server/account/config/rig.json    |  18 ++++
 server/account/package.json       |  25 ++++++
 server/account/src/index.ts       | 140 ++++++++++++++++++++++++++++++
 server/account/tsconfig.json      |   8 ++
 8 files changed, 232 insertions(+), 4 deletions(-)
 create mode 100644 server/account/.eslintrc.js
 create mode 100644 server/account/.npmignore
 create mode 100644 server/account/config/rig.json
 create mode 100644 server/account/package.json
 create mode 100644 server/account/src/index.ts
 create mode 100644 server/account/tsconfig.json

diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml
index 96aff10246..fab81f6b09 100644
--- a/common/config/rush/pnpm-lock.yaml
+++ b/common/config/rush/pnpm-lock.yaml
@@ -3,6 +3,7 @@ lockfileVersion: 5.3
 specifiers:
   '@elastic/elasticsearch': ^7.14.0
   '@microsoft/api-extractor': ^7.18.4
+  '@rush-temp/account': file:./projects/account.tgz
   '@rush-temp/chunter': file:./projects/chunter.tgz
   '@rush-temp/chunter-assets': file:./projects/chunter-assets.tgz
   '@rush-temp/chunter-resources': file:./projects/chunter-resources.tgz
@@ -72,6 +73,7 @@ specifiers:
   '@tiptap/extension-typography': ~2.0.0-beta.13
   '@tiptap/starter-kit': ~2.0.0-beta.89
   '@types/cors': ^2.8.12
+  '@types/deep-equal': ^1.0.1
   '@types/express': ^4.17.13
   '@types/express-fileupload': ^1.1.7
   '@types/heft-jest': ^1.0.2
@@ -99,7 +101,6 @@ specifiers:
   intl-messageformat: ^9.7.1
   mini-css-extract-plugin: ^2.2.0
   minio: ^7.0.19
-  mongodb: ^4.1.0
   node-html-parser: ^4.1.3
   postcss: ^8.3.4
   postcss-load-config: ^3.1.0
@@ -122,6 +123,7 @@ specifiers:
 dependencies:
   '@elastic/elasticsearch': 7.14.0
   '@microsoft/api-extractor': 7.18.4
+  '@rush-temp/account': file:projects/account.tgz_6c259fadfeb3a4b20890aefe87070b8b
   '@rush-temp/chunter': file:projects/chunter.tgz_6c259fadfeb3a4b20890aefe87070b8b
   '@rush-temp/chunter-assets': file:projects/chunter-assets.tgz
   '@rush-temp/chunter-resources': file:projects/chunter-resources.tgz_c38cf1a7a413db8918b0b4754c21e4c5
@@ -191,6 +193,7 @@ dependencies:
   '@tiptap/extension-typography': 2.0.0-beta.13_@tiptap+core@2.0.0-beta.93
   '@tiptap/starter-kit': 2.0.0-beta.89
   '@types/cors': 2.8.12
+  '@types/deep-equal': 1.0.1
   '@types/express': 4.17.13
   '@types/express-fileupload': 1.1.7
   '@types/heft-jest': 1.0.2
@@ -218,7 +221,6 @@ dependencies:
   intl-messageformat: 9.8.1
   mini-css-extract-plugin: 2.2.0_webpack@5.48.0
   minio: 7.0.19
-  mongodb: 4.1.1
   node-html-parser: 4.1.3
   postcss: 8.3.6
   postcss-load-config: 3.1.0
@@ -1670,7 +1672,7 @@ packages:
   /@types/whatwg-url/8.2.1:
     resolution: {integrity: sha512-2YubE1sjj5ifxievI5Ge1sckb9k/Er66HyR2c+3+I6VDUUg1TLPdYYTEbQ+DjRkS4nTxMJhgWfSfMRD2sl2EYQ==}
     dependencies:
-      '@types/node': 16.7.1
+      '@types/node': 16.7.5
       '@types/webidl-conversions': 6.1.1
     dev: false
 
@@ -9090,6 +9092,26 @@ packages:
       commander: 2.20.3
     dev: false
 
+  file:projects/account.tgz_6c259fadfeb3a4b20890aefe87070b8b:
+    resolution: {integrity: sha512-jykJZHqGILUDDIrPnSs2RQYlkU5YgPheSAJaxztshirE23ZgdqZRUb9VgqfHIhIRm6etoo0KliQCAurKoS6/kw==, tarball: file:projects/account.tgz}
+    id: file:projects/account.tgz
+    name: '@rush-temp/account'
+    version: 0.0.0
+    dependencies:
+      '@types/heft-jest': 1.0.2
+      '@typescript-eslint/eslint-plugin': 4.28.5_a8e83fcad666e1ba86be4b2e27a20aea
+      eslint: 7.32.0
+      eslint-plugin-import: 2.23.4_eslint@7.32.0
+      eslint-plugin-node: 11.1.0_eslint@7.32.0
+      eslint-plugin-promise: 4.3.1
+      jwt-simple: 0.5.6
+      mongodb: 4.1.1
+    transitivePeerDependencies:
+      - '@typescript-eslint/parser'
+      - supports-color
+      - typescript
+    dev: false
+
   file:projects/chunter-assets.tgz:
     resolution: {integrity: sha512-kMTEO1cP8cmeoA2tImDBtT2SXfjo4AW0MWEbzFvzhZkLZaMmJGtl5ZHclKn/Wwg/LBA1zDc21fKlpeahmuNS4A==, tarball: file:projects/chunter-assets.tgz}
     name: '@rush-temp/chunter-assets'
@@ -9834,7 +9856,7 @@ packages:
     dev: false
 
   file:projects/recruit-resources.tgz_c38cf1a7a413db8918b0b4754c21e4c5:
-    resolution: {integrity: sha512-ZTxU5DPTttq85I1jEwAbJ+RCuL1t9sFCQ0gJo/ax4VzfEN6M/6inkXZ5HQF812UD9vGcYlRF2+zqfnuoAfZxmA==, tarball: file:projects/recruit-resources.tgz}
+    resolution: {integrity: sha512-vsSwhXuC8vyxhYf1emb/0aTu4FCsXtuz2MPwgUskCveuUBtF2ROAqzo9Qy9HXZNHX1eOeDWVDcjbFmRuU4Peqg==, tarball: file:projects/recruit-resources.tgz}
     id: file:projects/recruit-resources.tgz
     name: '@rush-temp/recruit-resources'
     version: 0.0.0
diff --git a/rush.json b/rush.json
index a0fafd232b..c06204a7e3 100644
--- a/rush.json
+++ b/rush.json
@@ -746,5 +746,10 @@
       "projectFolder": "server/front",
       "shouldPublish": true
     },
+    {
+      "packageName": "@anticrm/account",
+      "projectFolder": "server/account",
+      "shouldPublish": true
+    },
   ]
 }
diff --git a/server/account/.eslintrc.js b/server/account/.eslintrc.js
new file mode 100644
index 0000000000..89f8151bd4
--- /dev/null
+++ b/server/account/.eslintrc.js
@@ -0,0 +1,6 @@
+module.exports = {
+  extends: ['./node_modules/@anticrm/platform-rig/profiles/default/config/eslint.config.json'],
+  parserOptions: {
+    project: './tsconfig.json'
+  }
+}
\ No newline at end of file
diff --git a/server/account/.npmignore b/server/account/.npmignore
new file mode 100644
index 0000000000..e3ec093c38
--- /dev/null
+++ b/server/account/.npmignore
@@ -0,0 +1,4 @@
+*
+!/lib/**
+!CHANGELOG.md
+/lib/**/__tests__/
diff --git a/server/account/config/rig.json b/server/account/config/rig.json
new file mode 100644
index 0000000000..af1257a896
--- /dev/null
+++ b/server/account/config/rig.json
@@ -0,0 +1,18 @@
+// The "rig.json" file directs tools to look for their config files in an external package.
+// Documentation for this system: https://www.npmjs.com/package/@rushstack/rig-package
+{
+  "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
+
+  /**
+   * (Required) The name of the rig package to inherit from.
+   * It should be an NPM package name with the "-rig" suffix.
+   */
+  "rigPackageName": "@anticrm/platform-rig"
+
+  /**
+   * (Optional) Selects a config profile from the rig package.  The name must consist of
+   * lowercase alphanumeric words separated by hyphens, for example "sample-profile".
+   * If omitted, then the "default" profile will be used."
+   */
+  // "rigProfile": "your-profile-name"
+}
diff --git a/server/account/package.json b/server/account/package.json
new file mode 100644
index 0000000000..05e7cbd891
--- /dev/null
+++ b/server/account/package.json
@@ -0,0 +1,25 @@
+{
+  "name": "@anticrm/account",
+  "version": "0.6.0",
+  "main": "lib/index.js",
+  "author": "Anticrm Platform Contributors",
+  "license": "EPL-2.0",
+  "scripts": {
+    "build": "heft build",
+    "lint:fix": "eslint --fix src"
+  },
+  "devDependencies": {
+    "@anticrm/platform-rig":"~0.6.0",
+    "@types/heft-jest":"^1.0.2",
+    "@typescript-eslint/eslint-plugin":"4",
+    "eslint-plugin-import":"2",
+    "eslint-plugin-promise":"4",
+    "eslint-plugin-node":"11",
+    "eslint":"^7.32.0"
+  },
+  "dependencies": {
+    "mongodb":"^4.1.1",
+    "@anticrm/platform":"~0.6.5",
+    "jwt-simple":"~0.5.6"
+  }
+}
diff --git a/server/account/src/index.ts b/server/account/src/index.ts
new file mode 100644
index 0000000000..9f35632483
--- /dev/null
+++ b/server/account/src/index.ts
@@ -0,0 +1,140 @@
+//
+// Copyright © 2020, 2021 Anticrm Platform Contributors.
+// Copyright © 2021 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 type { Plugin, StatusCode } from '@anticrm/platform'
+import { PlatformError, Severity, Status, plugin } from '@anticrm/platform'
+import { Binary, Db, ObjectId } from 'mongodb'
+import { pbkdf2Sync } from 'crypto'
+import { encode } from 'jwt-simple'
+
+const WORKSPACE_COLLECTION = 'workspace'
+const ACCOUNT_COLLECTION = 'account'
+
+const endpoint = 'wss://transactor.hc.engineering/'
+const secret = 'secret'
+
+/**
+ * @public
+ */
+export const accountId = 'account' as Plugin
+
+/**
+ * @public
+ */
+const accountPlugin = plugin(accountId, {
+  status: {
+    AccountNotFound: '' as StatusCode<{account: string}>,
+    InvalidPassword: '' as StatusCode<{account: string}>,
+    Forbidden: '' as StatusCode
+  }
+})
+
+interface Account {
+  _id: ObjectId
+  email: string
+  hash: Binary
+  salt: Binary
+  workspaces: ObjectId[]
+}
+
+interface Workspace {
+  _id: ObjectId
+  workspace: string
+  organisation: string
+  accounts: ObjectId[]
+}
+
+/**
+ * @public
+ */
+export interface LoginInfo {
+  email: string
+  token: string
+  endpoint: string
+}
+
+type AccountInfo = Omit<Account, 'hash' | 'salt'>
+
+function hashWithSalt (password: string, salt: Buffer): Buffer {
+  return pbkdf2Sync(password, salt, 1000, 32, 'sha256')
+}
+
+function verifyPassword (password: string, hash: Buffer, salt: Buffer): boolean {
+  return Buffer.compare(hash, hashWithSalt(password, salt)) === 0
+}
+
+async function getAccount (db: Db, email: string): Promise<Account | null> {
+  return await db.collection(ACCOUNT_COLLECTION).findOne<Account>({ email })
+}
+
+async function getWorkspace (db: Db, workspace: string): Promise<Workspace | null> {
+  return await db.collection(WORKSPACE_COLLECTION).findOne<Workspace>({
+    workspace
+  })
+}
+
+function toAccountInfo (account: Account): AccountInfo {
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  const { hash, salt, ...result } = account
+  return result
+}
+
+async function getAccountInfo (db: Db, email: string, password: string): Promise<AccountInfo> {
+  const account = await getAccount(db, email)
+  if (account === null) {
+    throw new PlatformError(new Status(Severity.ERROR, accountPlugin.status.AccountNotFound, { account: email }))
+  }
+  if (!verifyPassword(password, account.hash.buffer, account.salt.buffer)) {
+    throw new PlatformError(new Status(Severity.ERROR, accountPlugin.status.InvalidPassword, { account: email }))
+  }
+  return toAccountInfo(account)
+}
+
+function generateToken (email: string, workspace: string): string {
+  return encode({ email, workspace }, secret)
+}
+
+/**
+ * @public
+ * @param db -
+ * @param email -
+ * @param password -
+ * @param workspace -
+ * @returns
+ */
+export async function login (db: Db, email: string, password: string, workspace: string): Promise<LoginInfo> {
+  const accountInfo = await getAccountInfo(db, email, password)
+  const workspaceInfo = await getWorkspace(db, workspace)
+
+  if (workspaceInfo !== null) {
+    const workspaces = accountInfo.workspaces
+
+    for (const w of workspaces) {
+      if (w.equals(workspaceInfo._id)) {
+        const result = {
+          endpoint,
+          email,
+          token: generateToken(email, workspace)
+        }
+        return result
+      }
+    }
+  }
+
+  throw new PlatformError(new Status(Severity.ERROR, accountPlugin.status.Forbidden, {}))
+}
+
+export default accountPlugin
diff --git a/server/account/tsconfig.json b/server/account/tsconfig.json
new file mode 100644
index 0000000000..aeb0517b13
--- /dev/null
+++ b/server/account/tsconfig.json
@@ -0,0 +1,8 @@
+{
+  "extends": "./node_modules/@anticrm/platform-rig/profiles/default/tsconfig.json",
+
+  "compilerOptions": {
+    "rootDir": "./src",
+    "outDir": "./lib"
+  }
+}
\ No newline at end of file